slicejs-web-framework 3.3.8 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Slice/Components/Structural/ContextManager/ContextManager.js +2 -0
- package/Slice/Components/Structural/ContextManager/ContextManagerDebugger.js +60 -11
- package/Slice/Components/Structural/Debugger/Debugger.js +17 -16
- package/Slice/Components/Structural/EventManager/EventManager.js +32 -0
- package/Slice/Components/Structural/EventManager/EventManagerDebugger.js +210 -39
- package/Slice/Components/Structural/Logger/LogViewer/LogViewer.js +541 -0
- package/Slice/Components/Structural/Logger/Logger.js +25 -0
- package/Slice/Slice.js +19 -4
- package/package.json +1 -1
|
@@ -6,6 +6,8 @@ export default class ContextManagerDebugger extends HTMLElement {
|
|
|
6
6
|
super();
|
|
7
7
|
this.isOpen = false;
|
|
8
8
|
this.filterText = '';
|
|
9
|
+
this._contextSubscriptions = new Map();
|
|
10
|
+
this._debounceTimer = null;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
/**
|
|
@@ -18,6 +20,7 @@ export default class ContextManagerDebugger extends HTMLElement {
|
|
|
18
20
|
this.cacheElements();
|
|
19
21
|
this.bindEvents();
|
|
20
22
|
this.makeDraggable();
|
|
23
|
+
this._subscribeToAllContexts();
|
|
21
24
|
this.renderList();
|
|
22
25
|
}
|
|
23
26
|
|
|
@@ -29,6 +32,7 @@ export default class ContextManagerDebugger extends HTMLElement {
|
|
|
29
32
|
this.isOpen = !this.isOpen;
|
|
30
33
|
this.container.classList.toggle('active', this.isOpen);
|
|
31
34
|
if (this.isOpen) {
|
|
35
|
+
this._subscribeToAllContexts();
|
|
32
36
|
this.renderList();
|
|
33
37
|
}
|
|
34
38
|
}
|
|
@@ -40,6 +44,7 @@ export default class ContextManagerDebugger extends HTMLElement {
|
|
|
40
44
|
open() {
|
|
41
45
|
this.isOpen = true;
|
|
42
46
|
this.container.classList.add('active');
|
|
47
|
+
this._subscribeToAllContexts();
|
|
43
48
|
this.renderList();
|
|
44
49
|
}
|
|
45
50
|
|
|
@@ -52,6 +57,46 @@ export default class ContextManagerDebugger extends HTMLElement {
|
|
|
52
57
|
this.container.classList.remove('active');
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
disconnectedCallback() {
|
|
61
|
+
this._unsubscribeAll();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_unsubscribeAll() {
|
|
65
|
+
for (const [name, id] of this._contextSubscriptions) {
|
|
66
|
+
slice.events.unsubscribe(`context:${name}`, id);
|
|
67
|
+
}
|
|
68
|
+
this._contextSubscriptions.clear();
|
|
69
|
+
if (this._createdSub) {
|
|
70
|
+
slice.events.unsubscribe('context:__created', this._createdSub);
|
|
71
|
+
this._createdSub = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_subscribeToAllContexts() {
|
|
76
|
+
if (!slice?.context?.contexts) return;
|
|
77
|
+
slice.context.contexts.forEach((value, name) => {
|
|
78
|
+
if (this._contextSubscriptions.has(name)) return;
|
|
79
|
+
const id = slice.events.subscribe(`context:${name}`, () => this._scheduleRender());
|
|
80
|
+
this._contextSubscriptions.set(name, id);
|
|
81
|
+
});
|
|
82
|
+
if (!this._createdSub) {
|
|
83
|
+
this._createdSub = slice.events.subscribe('context:__created', ({ name }) => {
|
|
84
|
+
if (!this._contextSubscriptions.has(name)) {
|
|
85
|
+
const id = slice.events.subscribe(`context:${name}`, () => this._scheduleRender());
|
|
86
|
+
this._contextSubscriptions.set(name, id);
|
|
87
|
+
}
|
|
88
|
+
this._scheduleRender();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_scheduleRender() {
|
|
94
|
+
if (this._debounceTimer) clearTimeout(this._debounceTimer);
|
|
95
|
+
this._debounceTimer = setTimeout(() => {
|
|
96
|
+
if (this.isOpen) this.renderList();
|
|
97
|
+
}, 50);
|
|
98
|
+
}
|
|
99
|
+
|
|
55
100
|
cacheElements() {
|
|
56
101
|
this.container = this.querySelector('#context-debugger');
|
|
57
102
|
this.header = this.querySelector('.context-header');
|
|
@@ -178,18 +223,22 @@ export default class ContextManagerDebugger extends HTMLElement {
|
|
|
178
223
|
renderStyles() {
|
|
179
224
|
return `
|
|
180
225
|
/* Slice Instruments — context store. All selectors scoped to the
|
|
181
|
-
|
|
226
|
+
<slice-contextmanager-debugger> tag so nothing clashes with app styles.
|
|
227
|
+
Every --si-* token reads the matching framework theme variable from
|
|
228
|
+
:root, falling back to the original hardcoded value if absent. */
|
|
182
229
|
slice-contextmanager-debugger {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
230
|
+
--si-accent: var(--primary-color, #6ee7ff);
|
|
231
|
+
--si-accent-rgb: var(--primary-color-rgb, 110, 231, 255);
|
|
232
|
+
--si-surface: var(--primary-background-color, rgba(17, 19, 28, 0.86));
|
|
233
|
+
--si-raised: var(--secondary-background-color, rgba(255, 255, 255, 0.035));
|
|
234
|
+
--si-raised-2: var(--tertiary-background-color, rgba(255, 255, 255, 0.06));
|
|
235
|
+
--si-border: var(--medium-color, rgba(255, 255, 255, 0.09));
|
|
236
|
+
--si-text: var(--font-primary-color, #e8eaf2);
|
|
237
|
+
--si-dim: var(--font-secondary-color, #888fa6);
|
|
238
|
+
--si-danger: var(--danger-color, #ff6b6b);
|
|
239
|
+
--si-success: var(--success-color, #46d39a);
|
|
240
|
+
--si-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', 'Cascadia Code', Menlo, Consolas, monospace;
|
|
241
|
+
}
|
|
193
242
|
|
|
194
243
|
slice-contextmanager-debugger #context-debugger {
|
|
195
244
|
position: fixed;
|
|
@@ -811,24 +811,25 @@ return `
|
|
|
811
811
|
All selectors are scoped under the <slice-debugger> tag so the
|
|
812
812
|
panel never clashes with (or leaks into) app styles. Tokens live
|
|
813
813
|
on the tag so both #debugger-container and the sibling
|
|
814
|
-
#editor-modal inherit them.
|
|
815
|
-
|
|
814
|
+
#editor-modal inherit them. Every --si-* token reads the matching
|
|
815
|
+
framework theme variable from :root, falling back to the original
|
|
816
|
+
hardcoded value if absent — so debuggers always match the app theme.
|
|
816
817
|
============================================================ */
|
|
817
818
|
slice-debugger {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
}
|
|
819
|
+
--si-accent: var(--primary-color, #6ee7ff);
|
|
820
|
+
--si-accent-rgb: var(--primary-color-rgb, 110, 231, 255);
|
|
821
|
+
--si-surface: var(--primary-background-color, rgba(17, 19, 28, 0.88));
|
|
822
|
+
--si-raised: var(--secondary-background-color, rgba(255, 255, 255, 0.035));
|
|
823
|
+
--si-raised-2: var(--tertiary-background-color, rgba(255, 255, 255, 0.06));
|
|
824
|
+
--si-inset: var(--primary-color-shade, rgba(0, 0, 0, 0.28));
|
|
825
|
+
--si-border: var(--medium-color, rgba(255, 255, 255, 0.09));
|
|
826
|
+
--si-text: var(--font-primary-color, #e8eaf2);
|
|
827
|
+
--si-dim: var(--font-secondary-color, #888fa6);
|
|
828
|
+
--si-danger: var(--danger-color, #ff6b6b);
|
|
829
|
+
--si-success: var(--success-color, #46d39a);
|
|
830
|
+
--si-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', 'Cascadia Code', Menlo, Consolas, monospace;
|
|
831
|
+
--si-sans: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
832
|
+
}
|
|
832
833
|
|
|
833
834
|
slice-debugger *,
|
|
834
835
|
slice-debugger *::before,
|
|
@@ -23,6 +23,15 @@ export default class EventManager {
|
|
|
23
23
|
// Map<sliceId, Set<{ eventName, subscriptionId }>> - Para auto-cleanup
|
|
24
24
|
this.componentSubscriptions = new Map();
|
|
25
25
|
|
|
26
|
+
// Ring buffer de últimos emits (max 500) — solo se llena cuando el debugger está abierto
|
|
27
|
+
this.emitHistory = [];
|
|
28
|
+
|
|
29
|
+
// Contador de emits por evento en la sesión actual de grabación
|
|
30
|
+
this.emitCounts = new Map();
|
|
31
|
+
|
|
32
|
+
// Flag: solo grabamos cuando algún debugger UI está visible
|
|
33
|
+
this._recording = false;
|
|
34
|
+
|
|
26
35
|
// Contador para IDs únicos
|
|
27
36
|
this.idCounter = 0;
|
|
28
37
|
}
|
|
@@ -95,6 +104,22 @@ export default class EventManager {
|
|
|
95
104
|
* console.log('App lista!');
|
|
96
105
|
* });
|
|
97
106
|
*/
|
|
107
|
+
/**
|
|
108
|
+
* Activar registro de emits (lo llama el debugger al abrirse).
|
|
109
|
+
*/
|
|
110
|
+
startRecording() {
|
|
111
|
+
this._recording = true;
|
|
112
|
+
this.emitHistory = [];
|
|
113
|
+
this.emitCounts = new Map();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Desactivar registro de emits (lo llama el debugger al cerrarse).
|
|
118
|
+
*/
|
|
119
|
+
stopRecording() {
|
|
120
|
+
this._recording = false;
|
|
121
|
+
}
|
|
122
|
+
|
|
98
123
|
subscribeOnce(eventName, callback, options = {}) {
|
|
99
124
|
if (typeof callback !== 'function') {
|
|
100
125
|
slice.logger.logError('EventManager', 'El callback debe ser una función');
|
|
@@ -165,6 +190,13 @@ export default class EventManager {
|
|
|
165
190
|
emit(eventName, ...args) {
|
|
166
191
|
slice.logger.info('EventManager', `Emitting "${eventName}"`, args[0] ?? null);
|
|
167
192
|
|
|
193
|
+
// Solo grabamos el histórico si algún debugger está abierto
|
|
194
|
+
if (this._recording) {
|
|
195
|
+
this.emitHistory.push({ eventName, timestamp: Date.now() });
|
|
196
|
+
if (this.emitHistory.length > 500) this.emitHistory.shift();
|
|
197
|
+
this.emitCounts.set(eventName, (this.emitCounts.get(eventName) || 0) + 1);
|
|
198
|
+
}
|
|
199
|
+
|
|
168
200
|
if (!this.subscriptions.has(eventName)) {
|
|
169
201
|
return;
|
|
170
202
|
}
|
|
@@ -6,7 +6,10 @@ export default class EventManagerDebugger extends HTMLElement {
|
|
|
6
6
|
super();
|
|
7
7
|
this.isOpen = false;
|
|
8
8
|
this.filterText = '';
|
|
9
|
+
this.activeTab = 'subscribers';
|
|
9
10
|
this.refreshInterval = null;
|
|
11
|
+
this._autoRefreshTimer = null;
|
|
12
|
+
this._lastHistoryLength = 0;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
/**
|
|
@@ -41,6 +44,11 @@ export default class EventManagerDebugger extends HTMLElement {
|
|
|
41
44
|
open() {
|
|
42
45
|
this.isOpen = true;
|
|
43
46
|
this.container.classList.add('active');
|
|
47
|
+
this.activeTab = 'subscribers';
|
|
48
|
+
if (slice.events) {
|
|
49
|
+
slice.events.startRecording();
|
|
50
|
+
}
|
|
51
|
+
this._startAutoRefresh();
|
|
44
52
|
this.renderList();
|
|
45
53
|
}
|
|
46
54
|
|
|
@@ -51,6 +59,29 @@ export default class EventManagerDebugger extends HTMLElement {
|
|
|
51
59
|
close() {
|
|
52
60
|
this.isOpen = false;
|
|
53
61
|
this.container.classList.remove('active');
|
|
62
|
+
this._stopAutoRefresh();
|
|
63
|
+
if (slice.events) {
|
|
64
|
+
slice.events.stopRecording();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_startAutoRefresh() {
|
|
69
|
+
this._stopAutoRefresh();
|
|
70
|
+
this._autoRefreshTimer = setInterval(() => {
|
|
71
|
+
if (!this.isOpen) return;
|
|
72
|
+
if (this.activeTab === 'history') {
|
|
73
|
+
this.renderHistory();
|
|
74
|
+
} else {
|
|
75
|
+
this.renderList();
|
|
76
|
+
}
|
|
77
|
+
}, 1500);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_stopAutoRefresh() {
|
|
81
|
+
if (this._autoRefreshTimer) {
|
|
82
|
+
clearInterval(this._autoRefreshTimer);
|
|
83
|
+
this._autoRefreshTimer = null;
|
|
84
|
+
}
|
|
54
85
|
}
|
|
55
86
|
|
|
56
87
|
cacheElements() {
|
|
@@ -61,15 +92,35 @@ export default class EventManagerDebugger extends HTMLElement {
|
|
|
61
92
|
this.countLabel = this.querySelector('#events-count');
|
|
62
93
|
this.refreshButton = this.querySelector('#events-refresh');
|
|
63
94
|
this.closeButton = this.querySelector('#events-close');
|
|
95
|
+
this.tabSubs = this.querySelector('#tab-subscribers');
|
|
96
|
+
this.tabHistory = this.querySelector('#tab-history');
|
|
64
97
|
}
|
|
65
98
|
|
|
66
99
|
bindEvents() {
|
|
67
|
-
this.refreshButton.addEventListener('click', () =>
|
|
100
|
+
this.refreshButton.addEventListener('click', () => {
|
|
101
|
+
if (this.activeTab === 'history') this.renderHistory();
|
|
102
|
+
else this.renderList();
|
|
103
|
+
});
|
|
68
104
|
this.closeButton.addEventListener('click', () => this.close());
|
|
69
105
|
this.filterInput.addEventListener('input', (event) => {
|
|
70
106
|
this.filterText = event.target.value.trim().toLowerCase();
|
|
107
|
+
if (this.activeTab === 'history') this.renderHistory();
|
|
108
|
+
else this.renderList();
|
|
109
|
+
});
|
|
110
|
+
this.tabSubs.addEventListener('click', () => {
|
|
111
|
+
this.activeTab = 'subscribers';
|
|
112
|
+
this.tabSubs.classList.add('active');
|
|
113
|
+
this.tabHistory.classList.remove('active');
|
|
114
|
+
this.filterInput.placeholder = 'filter events…';
|
|
71
115
|
this.renderList();
|
|
72
116
|
});
|
|
117
|
+
this.tabHistory.addEventListener('click', () => {
|
|
118
|
+
this.activeTab = 'history';
|
|
119
|
+
this.tabHistory.classList.add('active');
|
|
120
|
+
this.tabSubs.classList.remove('active');
|
|
121
|
+
this.filterInput.placeholder = 'filter history…';
|
|
122
|
+
this.renderHistory();
|
|
123
|
+
});
|
|
73
124
|
}
|
|
74
125
|
|
|
75
126
|
makeDraggable() {
|
|
@@ -117,6 +168,7 @@ export default class EventManagerDebugger extends HTMLElement {
|
|
|
117
168
|
|
|
118
169
|
const items = [];
|
|
119
170
|
slice.events.subscriptions.forEach((subs, eventName) => {
|
|
171
|
+
const emitCount = slice.events.emitCounts?.get(eventName) || 0;
|
|
120
172
|
const entries = Array.from(subs.entries()).map(([id, sub]) => {
|
|
121
173
|
const componentSliceId = sub.componentSliceId || null;
|
|
122
174
|
const component = componentSliceId ? slice.controller.getComponent(componentSliceId) : null;
|
|
@@ -133,7 +185,7 @@ export default class EventManagerDebugger extends HTMLElement {
|
|
|
133
185
|
return;
|
|
134
186
|
}
|
|
135
187
|
|
|
136
|
-
items.push({ eventName, count: subs.size, entries });
|
|
188
|
+
items.push({ eventName, count: subs.size, emitCount, entries });
|
|
137
189
|
});
|
|
138
190
|
|
|
139
191
|
items.sort((a, b) => a.eventName.localeCompare(b.eventName));
|
|
@@ -141,32 +193,76 @@ export default class EventManagerDebugger extends HTMLElement {
|
|
|
141
193
|
this.countLabel.textContent = String(items.length);
|
|
142
194
|
this.list.innerHTML = items.length
|
|
143
195
|
? items.map((item) => {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
196
|
+
const details = item.entries.map((entry) => {
|
|
197
|
+
const label = entry.componentName
|
|
198
|
+
? `${entry.componentName} (${entry.componentSliceId})`
|
|
199
|
+
: entry.componentSliceId || 'Global';
|
|
200
|
+
const onceBadge = entry.once ? '<span class="badge">once</span>' : '';
|
|
201
|
+
return `
|
|
202
|
+
<div class="subscriber-row">
|
|
203
|
+
<div class="subscriber-name">${label}</div>
|
|
204
|
+
<div class="subscriber-meta">${entry.id}${onceBadge}</div>
|
|
205
|
+
</div>
|
|
206
|
+
`;
|
|
207
|
+
}).join('');
|
|
208
|
+
|
|
209
|
+
return `
|
|
210
|
+
<details class="event-row">
|
|
211
|
+
<summary>
|
|
212
|
+
<div class="event-name">${item.eventName}</div>
|
|
213
|
+
<div class="event-metrics">
|
|
214
|
+
<span class="emit-count" title="Emits this session">⚡${item.emitCount}</span>
|
|
215
|
+
<span class="event-count">${item.count}</span>
|
|
216
|
+
</div>
|
|
217
|
+
</summary>
|
|
218
|
+
<div class="subscriber-list">
|
|
219
|
+
${details || '<div class="empty">No subscribers</div>'}
|
|
220
|
+
</div>
|
|
221
|
+
</details>
|
|
222
|
+
`;
|
|
223
|
+
}).join('')
|
|
224
|
+
: '<div class="empty">No events</div>';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
renderHistory() {
|
|
228
|
+
if (!slice?.events?.emitHistory) {
|
|
229
|
+
this.list.textContent = 'EventManager not available.';
|
|
230
|
+
this.countLabel.textContent = '0';
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const history = slice.events.emitHistory;
|
|
235
|
+
const filtered = this.filterText
|
|
236
|
+
? history.filter(e => e.eventName.toLowerCase().includes(this.filterText))
|
|
237
|
+
: history;
|
|
238
|
+
|
|
239
|
+
this.countLabel.textContent = String(filtered.length);
|
|
240
|
+
if (!filtered.length) {
|
|
241
|
+
this.list.innerHTML = '<div class="empty">No emits recorded yet</div>';
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const now = Date.now();
|
|
246
|
+
this.list.innerHTML = [...filtered].reverse().map((entry) => {
|
|
247
|
+
const diff = now - entry.timestamp;
|
|
248
|
+
const timeStr = diff < 1000 ? 'now'
|
|
249
|
+
: diff < 60000 ? `${Math.floor(diff / 1000)}s ago`
|
|
250
|
+
: `${Math.floor(diff / 60000)}m ago`;
|
|
251
|
+
return `
|
|
252
|
+
<div class="history-row">
|
|
253
|
+
<div class="history-event">${this.escapeHtml(entry.eventName)}</div>
|
|
254
|
+
<div class="history-time">${timeStr}</div>
|
|
255
|
+
</div>
|
|
256
|
+
`;
|
|
257
|
+
}).join('');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
escapeHtml(value) {
|
|
261
|
+
return String(value)
|
|
262
|
+
.replace(/&/g, '&')
|
|
263
|
+
.replace(/</g, '<')
|
|
264
|
+
.replace(/>/g, '>')
|
|
265
|
+
.replace(/"/g, '"');
|
|
170
266
|
}
|
|
171
267
|
|
|
172
268
|
renderTemplate() {
|
|
@@ -183,6 +279,10 @@ export default class EventManagerDebugger extends HTMLElement {
|
|
|
183
279
|
<button id="events-close" class="btn" title="Close" aria-label="Close">✕</button>
|
|
184
280
|
</div>
|
|
185
281
|
</div>
|
|
282
|
+
<div class="events-tabs">
|
|
283
|
+
<button id="tab-subscribers" class="tab-btn active">Subscribers</button>
|
|
284
|
+
<button id="tab-history" class="tab-btn">History</button>
|
|
285
|
+
</div>
|
|
186
286
|
<div class="events-toolbar">
|
|
187
287
|
<input id="events-filter" type="text" placeholder="filter events…" autocomplete="off" spellcheck="false" />
|
|
188
288
|
<div class="count"><span id="events-count">0</span></div>
|
|
@@ -195,18 +295,22 @@ export default class EventManagerDebugger extends HTMLElement {
|
|
|
195
295
|
renderStyles() {
|
|
196
296
|
return `
|
|
197
297
|
/* Slice Instruments — events console. All selectors scoped to the
|
|
198
|
-
|
|
298
|
+
<slice-eventmanager-debugger> tag so nothing clashes with app styles.
|
|
299
|
+
Every --si-* token reads the matching framework theme variable from
|
|
300
|
+
:root, falling back to the original hardcoded value if absent. */
|
|
199
301
|
slice-eventmanager-debugger {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
302
|
+
--si-accent: var(--primary-color, #6ee7ff);
|
|
303
|
+
--si-accent-rgb: var(--primary-color-rgb, 110, 231, 255);
|
|
304
|
+
--si-surface: var(--primary-background-color, rgba(17, 19, 28, 0.86));
|
|
305
|
+
--si-raised: var(--secondary-background-color, rgba(255, 255, 255, 0.035));
|
|
306
|
+
--si-raised-2: var(--tertiary-background-color, rgba(255, 255, 255, 0.06));
|
|
307
|
+
--si-border: var(--medium-color, rgba(255, 255, 255, 0.09));
|
|
308
|
+
--si-text: var(--font-primary-color, #e8eaf2);
|
|
309
|
+
--si-dim: var(--font-secondary-color, #888fa6);
|
|
310
|
+
--si-danger: var(--danger-color, #ff6b6b);
|
|
311
|
+
--si-success: var(--success-color, #46d39a);
|
|
312
|
+
--si-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', 'Cascadia Code', Menlo, Consolas, monospace;
|
|
313
|
+
}
|
|
210
314
|
|
|
211
315
|
slice-eventmanager-debugger #events-debugger {
|
|
212
316
|
position: fixed;
|
|
@@ -311,6 +415,31 @@ slice-eventmanager-debugger .btn:hover {
|
|
|
311
415
|
slice-eventmanager-debugger .btn:active { transform: scale(0.92); }
|
|
312
416
|
slice-eventmanager-debugger #events-refresh:hover { color: var(--si-accent); }
|
|
313
417
|
|
|
418
|
+
slice-eventmanager-debugger .events-tabs {
|
|
419
|
+
display: flex;
|
|
420
|
+
gap: 0;
|
|
421
|
+
padding: 0 12px;
|
|
422
|
+
border-bottom: 1px solid var(--si-border);
|
|
423
|
+
background: var(--si-raised);
|
|
424
|
+
}
|
|
425
|
+
slice-eventmanager-debugger .tab-btn {
|
|
426
|
+
all: unset;
|
|
427
|
+
cursor: pointer;
|
|
428
|
+
font-size: 10px;
|
|
429
|
+
font-weight: 600;
|
|
430
|
+
letter-spacing: 0.06em;
|
|
431
|
+
text-transform: uppercase;
|
|
432
|
+
padding: 7px 12px;
|
|
433
|
+
color: var(--si-dim);
|
|
434
|
+
border-bottom: 2px solid transparent;
|
|
435
|
+
transition: color 0.15s ease, border-color 0.15s ease;
|
|
436
|
+
}
|
|
437
|
+
slice-eventmanager-debugger .tab-btn:hover { color: var(--si-text); }
|
|
438
|
+
slice-eventmanager-debugger .tab-btn.active {
|
|
439
|
+
color: var(--si-accent);
|
|
440
|
+
border-bottom-color: var(--si-accent);
|
|
441
|
+
}
|
|
442
|
+
|
|
314
443
|
slice-eventmanager-debugger .events-toolbar {
|
|
315
444
|
display: flex;
|
|
316
445
|
gap: 10px;
|
|
@@ -397,6 +526,21 @@ slice-eventmanager-debugger .event-name {
|
|
|
397
526
|
white-space: nowrap;
|
|
398
527
|
}
|
|
399
528
|
|
|
529
|
+
slice-eventmanager-debugger .event-metrics {
|
|
530
|
+
display: flex;
|
|
531
|
+
align-items: center;
|
|
532
|
+
gap: 6px;
|
|
533
|
+
}
|
|
534
|
+
slice-eventmanager-debugger .emit-count {
|
|
535
|
+
font-weight: 600;
|
|
536
|
+
font-size: 10px;
|
|
537
|
+
color: var(--si-success);
|
|
538
|
+
background: rgba(70, 211, 154, 0.12);
|
|
539
|
+
border: 1px solid rgba(70, 211, 154, 0.25);
|
|
540
|
+
padding: 1px 6px;
|
|
541
|
+
border-radius: 999px;
|
|
542
|
+
white-space: nowrap;
|
|
543
|
+
}
|
|
400
544
|
slice-eventmanager-debugger .event-count {
|
|
401
545
|
font-weight: 600;
|
|
402
546
|
font-size: 11px;
|
|
@@ -408,6 +552,33 @@ slice-eventmanager-debugger .event-count {
|
|
|
408
552
|
min-width: 22px;
|
|
409
553
|
text-align: center;
|
|
410
554
|
}
|
|
555
|
+
slice-eventmanager-debugger .history-row {
|
|
556
|
+
display: flex;
|
|
557
|
+
justify-content: space-between;
|
|
558
|
+
align-items: center;
|
|
559
|
+
gap: 10px;
|
|
560
|
+
padding: 6px 11px;
|
|
561
|
+
background: var(--si-raised);
|
|
562
|
+
border-radius: 7px;
|
|
563
|
+
border: 1px solid var(--si-border);
|
|
564
|
+
border-left: 2px solid transparent;
|
|
565
|
+
transition: border-color 0.15s ease;
|
|
566
|
+
}
|
|
567
|
+
slice-eventmanager-debugger .history-row:hover { border-left-color: var(--si-accent); }
|
|
568
|
+
slice-eventmanager-debugger .history-event {
|
|
569
|
+
font-family: var(--si-mono);
|
|
570
|
+
font-size: 11.5px;
|
|
571
|
+
color: var(--si-text);
|
|
572
|
+
overflow: hidden;
|
|
573
|
+
text-overflow: ellipsis;
|
|
574
|
+
white-space: nowrap;
|
|
575
|
+
}
|
|
576
|
+
slice-eventmanager-debugger .history-time {
|
|
577
|
+
font-size: 10px;
|
|
578
|
+
color: var(--si-dim);
|
|
579
|
+
white-space: nowrap;
|
|
580
|
+
flex-shrink: 0;
|
|
581
|
+
}
|
|
411
582
|
|
|
412
583
|
slice-eventmanager-debugger .subscriber-list {
|
|
413
584
|
margin-top: 9px;
|
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
const LV_LOG_TYPES = ['error', 'warn', 'info', 'debug'];
|
|
2
|
+
|
|
3
|
+
const LV_BADGE_LABEL = { error: 'ERR', warn: 'WRN', info: 'INF', debug: 'DBG' };
|
|
4
|
+
|
|
5
|
+
function pad2(n) {
|
|
6
|
+
return String(n).padStart(2, '0');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function formatTime(d) {
|
|
10
|
+
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function escapeHtml(str) {
|
|
14
|
+
if (!str) return '';
|
|
15
|
+
return String(str).replace(/[&<>"']/g, function (m) {
|
|
16
|
+
if (m === '&') return '&';
|
|
17
|
+
if (m === '<') return '<';
|
|
18
|
+
if (m === '>') return '>';
|
|
19
|
+
if (m === '"') return '"';
|
|
20
|
+
return ''';
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default class LogViewer extends HTMLElement {
|
|
25
|
+
constructor() {
|
|
26
|
+
super();
|
|
27
|
+
|
|
28
|
+
slice.stylesManager.registerComponentStyles('LogViewer', productionOnlyCSS());
|
|
29
|
+
this.innerHTML = productionOnlyHtml();
|
|
30
|
+
|
|
31
|
+
this.$header = this.querySelector('.lv__header');
|
|
32
|
+
this.$body = this.querySelector('[data-body]');
|
|
33
|
+
this.$count = this.querySelector('[data-count]');
|
|
34
|
+
|
|
35
|
+
this._isOpen = false;
|
|
36
|
+
this._logCount = 0;
|
|
37
|
+
this._filters = new Set();
|
|
38
|
+
this._dragState = null;
|
|
39
|
+
this._logHandler = null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
init() {
|
|
43
|
+
this._attachDrag();
|
|
44
|
+
this._attachFilterClicks();
|
|
45
|
+
this._attachClear();
|
|
46
|
+
this._attachMinimize();
|
|
47
|
+
this._attachBodyClick();
|
|
48
|
+
this._render();
|
|
49
|
+
this._subscribeLogger();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
toggle() {
|
|
53
|
+
this._isOpen = !this._isOpen;
|
|
54
|
+
this.style.display = this._isOpen ? '' : 'none';
|
|
55
|
+
if (this._isOpen) {
|
|
56
|
+
this._subscribeLogger();
|
|
57
|
+
this._render();
|
|
58
|
+
} else {
|
|
59
|
+
this._unsubscribeLogger();
|
|
60
|
+
}
|
|
61
|
+
return this._isOpen;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_detach() {
|
|
65
|
+
this._unsubscribeLogger();
|
|
66
|
+
if (this._dragCleanup) this._dragCleanup();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* -- drag -- */
|
|
70
|
+
_attachDrag() {
|
|
71
|
+
const header = this.$header;
|
|
72
|
+
if (!header) return;
|
|
73
|
+
|
|
74
|
+
const onDown = (e) => {
|
|
75
|
+
if (e.target.closest('button')) return;
|
|
76
|
+
this._dragState = {
|
|
77
|
+
startX: e.clientX,
|
|
78
|
+
startY: e.clientY,
|
|
79
|
+
origLeft: this.offsetLeft,
|
|
80
|
+
origTop: this.offsetTop,
|
|
81
|
+
origRight: this.style.right,
|
|
82
|
+
origBottom: this.style.bottom
|
|
83
|
+
};
|
|
84
|
+
this.style.right = 'auto';
|
|
85
|
+
this.style.bottom = 'auto';
|
|
86
|
+
document.addEventListener('mousemove', onMove);
|
|
87
|
+
document.addEventListener('mouseup', onUp);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const onMove = (e) => {
|
|
91
|
+
if (!this._dragState) return;
|
|
92
|
+
const dx = e.clientX - this._dragState.startX;
|
|
93
|
+
const dy = e.clientY - this._dragState.startY;
|
|
94
|
+
this.style.left = (this._dragState.origLeft + dx) + 'px';
|
|
95
|
+
this.style.top = (this._dragState.origTop + dy) + 'px';
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const onUp = () => {
|
|
99
|
+
this._dragState = null;
|
|
100
|
+
document.removeEventListener('mousemove', onMove);
|
|
101
|
+
document.removeEventListener('mouseup', onUp);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
header.addEventListener('mousedown', onDown);
|
|
105
|
+
this._dragCleanup = () => {
|
|
106
|
+
header.removeEventListener('mousedown', onDown);
|
|
107
|
+
document.removeEventListener('mousemove', onMove);
|
|
108
|
+
document.removeEventListener('mouseup', onUp);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* -- filter buttons -- */
|
|
113
|
+
_attachFilterClicks() {
|
|
114
|
+
const btns = this.querySelectorAll('.lv__filter');
|
|
115
|
+
for (const btn of btns) {
|
|
116
|
+
btn.addEventListener('click', () => {
|
|
117
|
+
const type = btn.dataset.filter;
|
|
118
|
+
if (btn.hasAttribute('data-active')) {
|
|
119
|
+
btn.removeAttribute('data-active');
|
|
120
|
+
this._filters.delete(type);
|
|
121
|
+
} else {
|
|
122
|
+
btn.setAttribute('data-active', '');
|
|
123
|
+
this._filters.add(type);
|
|
124
|
+
}
|
|
125
|
+
this._render();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* -- clear -- */
|
|
131
|
+
_attachClear() {
|
|
132
|
+
const btn = this.querySelector('.lv__clear');
|
|
133
|
+
if (!btn) return;
|
|
134
|
+
btn.addEventListener('click', () => {
|
|
135
|
+
if (slice.logger) slice.logger.logs = [];
|
|
136
|
+
this._logCount = 0;
|
|
137
|
+
this._render();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* -- minimize -- */
|
|
142
|
+
_attachMinimize() {
|
|
143
|
+
const btn = this.querySelector('.lv__close');
|
|
144
|
+
if (!btn) return;
|
|
145
|
+
btn.addEventListener('click', () => {
|
|
146
|
+
if (this.hasAttribute('data-minimized')) {
|
|
147
|
+
this.removeAttribute('data-minimized');
|
|
148
|
+
btn.textContent = '\u00d7';
|
|
149
|
+
} else {
|
|
150
|
+
this.setAttribute('data-minimized', '');
|
|
151
|
+
btn.textContent = '\u25a1';
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* -- expand error detail on click -- */
|
|
157
|
+
_attachBodyClick() {
|
|
158
|
+
if (!this.$body) return;
|
|
159
|
+
this.$body.addEventListener('click', (e) => {
|
|
160
|
+
const entry = e.target.closest('.lv__entry');
|
|
161
|
+
if (!entry) return;
|
|
162
|
+
const hasErr = entry.querySelector('.lv__entry-error');
|
|
163
|
+
if (!hasErr) return;
|
|
164
|
+
entry.toggleAttribute('data-expanded');
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* -- real-time subscription via logger.onLog -- */
|
|
169
|
+
_subscribeLogger() {
|
|
170
|
+
const logger = slice.logger;
|
|
171
|
+
if (!logger || typeof logger.onLog !== 'function') return;
|
|
172
|
+
this._logHandler = logger.onLog(() => {
|
|
173
|
+
this._render();
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
_unsubscribeLogger() {
|
|
178
|
+
const logger = slice.logger;
|
|
179
|
+
if (!logger || typeof logger.offLog !== 'function') return;
|
|
180
|
+
if (this._logHandler) {
|
|
181
|
+
logger.offLog(this._logHandler);
|
|
182
|
+
this._logHandler = null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* -- render -- */
|
|
187
|
+
_render() {
|
|
188
|
+
const logger = slice.logger;
|
|
189
|
+
if (!logger || !logger.logs) return;
|
|
190
|
+
const logs = logger.logs;
|
|
191
|
+
this._logCount = logs.length;
|
|
192
|
+
|
|
193
|
+
const filters = this._filters;
|
|
194
|
+
const hasFilters = filters.size > 0;
|
|
195
|
+
|
|
196
|
+
if (this.$count) {
|
|
197
|
+
this.$count.textContent = logs.length;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!this.$body) return;
|
|
201
|
+
|
|
202
|
+
if (logs.length === 0) {
|
|
203
|
+
this.$body.innerHTML = '<div class="lv__empty"><span class="lv__empty-icon">📡</span><span>No logs yet</span></div>';
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const filtered = hasFilters ? logs.filter((log) => filters.has(log.logType)) : logs;
|
|
208
|
+
const reversed = [...filtered].reverse();
|
|
209
|
+
|
|
210
|
+
let html = '';
|
|
211
|
+
for (const log of reversed) {
|
|
212
|
+
const type = log.logType || 'info';
|
|
213
|
+
const ts = formatTime(log.timestamp instanceof Date ? log.timestamp : new Date(log.timestamp));
|
|
214
|
+
const badge = LV_BADGE_LABEL[type] || 'INF';
|
|
215
|
+
const component = escapeHtml(log.componentSliceId || '');
|
|
216
|
+
const msg = escapeHtml(log.message || '');
|
|
217
|
+
|
|
218
|
+
let errHtml = '';
|
|
219
|
+
if (log.error) {
|
|
220
|
+
const stack = log.error.stack || String(log.error);
|
|
221
|
+
errHtml = `<div class="lv__entry-error">${escapeHtml(stack)}</div>`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
html += `<div class="lv__entry lv__entry--${type}">
|
|
225
|
+
<span class="lv__entry-time">${ts}</span>
|
|
226
|
+
<span class="lv__entry-badge">${badge}</span>
|
|
227
|
+
<div class="lv__entry-body">
|
|
228
|
+
<div class="lv__entry-component">${component}</div>
|
|
229
|
+
<div class="lv__entry-message">${msg}</div>
|
|
230
|
+
${errHtml}
|
|
231
|
+
</div>
|
|
232
|
+
</div>`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
this.$body.innerHTML = html;
|
|
236
|
+
if (this.$body.scrollTop < 20) {
|
|
237
|
+
this.$body.scrollTop = 0;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
customElements.define('slice-log-viewer', LogViewer);
|
|
243
|
+
|
|
244
|
+
function productionOnlyCSS() {
|
|
245
|
+
return `/* ── LogViewer (Structural) ── */
|
|
246
|
+
slice-log-viewer {
|
|
247
|
+
--si-accent: var(--primary-color, #6ee7ff);
|
|
248
|
+
--si-accent-rgb: var(--primary-color-rgb, 110, 231, 255);
|
|
249
|
+
--si-surface: var(--primary-background-color, #1e1e2e);
|
|
250
|
+
--si-raised: var(--secondary-background-color, rgba(255, 255, 255, 0.035));
|
|
251
|
+
--si-raised-2: var(--tertiary-background-color, #2a2a3e);
|
|
252
|
+
--si-border: var(--medium-color, #555);
|
|
253
|
+
--si-text: var(--font-primary-color, #cdd6f4);
|
|
254
|
+
--si-dim: var(--font-secondary-color, #a6adc8);
|
|
255
|
+
--si-danger: var(--danger-color, #f38ba8);
|
|
256
|
+
--si-warning: var(--warning-color, #f9e2af);
|
|
257
|
+
--si-info: var(--secondary-color, #89dceb);
|
|
258
|
+
--si-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
|
259
|
+
position: fixed;
|
|
260
|
+
z-index: 2147483647;
|
|
261
|
+
bottom: 24px;
|
|
262
|
+
right: 24px;
|
|
263
|
+
width: 520px;
|
|
264
|
+
max-width: calc(100vw - 48px);
|
|
265
|
+
height: 380px;
|
|
266
|
+
max-height: calc(100vh - 48px);
|
|
267
|
+
display: block;
|
|
268
|
+
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
|
269
|
+
font-size: 12px;
|
|
270
|
+
line-height: 1.5;
|
|
271
|
+
border: 1px solid color-mix(in srgb, var(--si-border, #555) 60%, transparent);
|
|
272
|
+
border-radius: 12px;
|
|
273
|
+
background: color-mix(in srgb, var(--si-surface, #1e1e2e) 97%, #000);
|
|
274
|
+
box-shadow: 0 12px 48px rgba(0,0,0,.45), 0 0 0 1px rgba(0,0,0,.08);
|
|
275
|
+
overflow: hidden;
|
|
276
|
+
user-select: none;
|
|
277
|
+
resize: both;
|
|
278
|
+
min-width: 280px;
|
|
279
|
+
min-height: 200px;
|
|
280
|
+
transition: opacity .18s ease, transform .18s ease;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
slice-log-viewer[data-minimized] {
|
|
284
|
+
height: auto !important;
|
|
285
|
+
resize: none;
|
|
286
|
+
min-height: 0;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
slice-log-viewer[data-minimized] .lv__body {
|
|
290
|
+
display: none;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.lv__header {
|
|
294
|
+
display: flex;
|
|
295
|
+
align-items: center;
|
|
296
|
+
justify-content: space-between;
|
|
297
|
+
gap: 8px;
|
|
298
|
+
padding: 8px 10px 8px 14px;
|
|
299
|
+
background: color-mix(in srgb, var(--si-raised-2, #2a2a3e) 80%, #000);
|
|
300
|
+
border-bottom: 1px solid color-mix(in srgb, var(--si-border, #555) 40%, transparent);
|
|
301
|
+
cursor: grab;
|
|
302
|
+
position: relative;
|
|
303
|
+
z-index: 1;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.lv__header:active { cursor: grabbing; }
|
|
307
|
+
|
|
308
|
+
.lv__header-left {
|
|
309
|
+
display: flex;
|
|
310
|
+
align-items: center;
|
|
311
|
+
gap: 8px;
|
|
312
|
+
min-width: 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.lv__title {
|
|
316
|
+
font-size: 13px;
|
|
317
|
+
font-weight: 700;
|
|
318
|
+
letter-spacing: .02em;
|
|
319
|
+
color: var(--si-text, #cdd6f4);
|
|
320
|
+
white-space: nowrap;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.lv__count {
|
|
324
|
+
font-size: 10px;
|
|
325
|
+
font-weight: 600;
|
|
326
|
+
padding: 1px 6px;
|
|
327
|
+
border-radius: 6px;
|
|
328
|
+
background: color-mix(in srgb, var(--si-border, #555) 40%, transparent);
|
|
329
|
+
color: var(--si-dim, #a6adc8);
|
|
330
|
+
font-variant-numeric: tabular-nums;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.lv__header-actions {
|
|
334
|
+
display: flex;
|
|
335
|
+
align-items: center;
|
|
336
|
+
gap: 4px;
|
|
337
|
+
flex-shrink: 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.lv__filter,
|
|
341
|
+
.lv__clear,
|
|
342
|
+
.lv__close {
|
|
343
|
+
all: unset;
|
|
344
|
+
cursor: pointer;
|
|
345
|
+
font-size: 10px;
|
|
346
|
+
font-weight: 600;
|
|
347
|
+
padding: 3px 7px;
|
|
348
|
+
border-radius: 6px;
|
|
349
|
+
color: var(--si-dim, #a6adc8);
|
|
350
|
+
background: color-mix(in srgb, var(--si-border, #555) 25%, transparent);
|
|
351
|
+
transition: background .12s ease, color .12s ease;
|
|
352
|
+
font-family: inherit;
|
|
353
|
+
line-height: 1.4;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.lv__filter:hover,
|
|
357
|
+
.lv__clear:hover,
|
|
358
|
+
.lv__close:hover {
|
|
359
|
+
background: color-mix(in srgb, var(--si-border, #555) 45%, transparent);
|
|
360
|
+
color: var(--si-text, #cdd6f4);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.lv__filter[data-active] { color: var(--si-text, #fff); }
|
|
364
|
+
|
|
365
|
+
.lv__filter[data-active][data-filter="error"] {
|
|
366
|
+
background: var(--si-danger, #f38ba8);
|
|
367
|
+
color: var(--danger-contrast, #1e1e2e);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.lv__filter[data-active][data-filter="warn"] {
|
|
371
|
+
background: var(--si-warning, #f9e2af);
|
|
372
|
+
color: var(--warning-contrast, #1e1e2e);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.lv__filter[data-active][data-filter="info"] {
|
|
376
|
+
background: var(--si-info, #89dceb);
|
|
377
|
+
color: var(--secondary-color-contrast, #1e1e2e);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.lv__filter[data-active][data-filter="debug"] {
|
|
381
|
+
background: var(--si-dim, #a6adc8);
|
|
382
|
+
color: var(--si-text, #1e1e2e);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.lv__clear { margin-left: 2px; }
|
|
386
|
+
|
|
387
|
+
.lv__close {
|
|
388
|
+
font-size: 16px;
|
|
389
|
+
line-height: 1;
|
|
390
|
+
padding: 3px 8px 4px;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.lv__body {
|
|
394
|
+
overflow-y: auto;
|
|
395
|
+
overflow-x: hidden;
|
|
396
|
+
height: calc(100% - 38px);
|
|
397
|
+
padding: 4px 0;
|
|
398
|
+
cursor: auto;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.lv__body::-webkit-scrollbar { width: 5px; }
|
|
402
|
+
.lv__body::-webkit-scrollbar-track { background: transparent; }
|
|
403
|
+
.lv__body::-webkit-scrollbar-thumb {
|
|
404
|
+
background: color-mix(in srgb, var(--si-border, #555) 40%, transparent);
|
|
405
|
+
border-radius: 3px;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.lv__entry {
|
|
409
|
+
display: flex;
|
|
410
|
+
align-items: flex-start;
|
|
411
|
+
gap: 8px;
|
|
412
|
+
padding: 5px 14px;
|
|
413
|
+
border-bottom: 1px solid color-mix(in srgb, var(--si-border, #555) 12%, transparent);
|
|
414
|
+
transition: background .1s ease;
|
|
415
|
+
cursor: pointer;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.lv__entry:hover {
|
|
419
|
+
background: color-mix(in srgb, var(--si-border, #555) 8%, transparent);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.lv__entry:last-child { border-bottom: none; }
|
|
423
|
+
|
|
424
|
+
.lv__entry-time {
|
|
425
|
+
font-size: 10px;
|
|
426
|
+
color: color-mix(in srgb, var(--si-dim, #a6adc8) 55%, transparent);
|
|
427
|
+
white-space: nowrap;
|
|
428
|
+
flex-shrink: 0;
|
|
429
|
+
font-variant-numeric: tabular-nums;
|
|
430
|
+
margin-top: 1px;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.lv__entry-badge {
|
|
434
|
+
font-size: 9px;
|
|
435
|
+
font-weight: 700;
|
|
436
|
+
padding: 1px 5px;
|
|
437
|
+
border-radius: 4px;
|
|
438
|
+
text-transform: uppercase;
|
|
439
|
+
letter-spacing: .04em;
|
|
440
|
+
flex-shrink: 0;
|
|
441
|
+
margin-top: 1px;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.lv__entry--error .lv__entry-badge {
|
|
445
|
+
background: var(--si-danger, #f38ba8);
|
|
446
|
+
color: var(--danger-contrast, #1e1e2e);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.lv__entry--warn .lv__entry-badge {
|
|
450
|
+
background: var(--si-warning, #f9e2af);
|
|
451
|
+
color: var(--warning-contrast, #1e1e2e);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.lv__entry--info .lv__entry-badge {
|
|
455
|
+
background: var(--si-info, #89dceb);
|
|
456
|
+
color: var(--secondary-color-contrast, #1e1e2e);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.lv__entry--debug .lv__entry-badge {
|
|
460
|
+
background: color-mix(in srgb, var(--si-border, #555) 40%, transparent);
|
|
461
|
+
color: var(--si-dim, #a6adc8);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.lv__entry-body {
|
|
465
|
+
flex: 1;
|
|
466
|
+
min-width: 0;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.lv__entry-component {
|
|
470
|
+
font-size: 10px;
|
|
471
|
+
font-weight: 600;
|
|
472
|
+
color: var(--si-text, #cdd6f4);
|
|
473
|
+
margin-bottom: 1px;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.lv__entry-message {
|
|
477
|
+
font-size: 11px;
|
|
478
|
+
color: var(--si-dim, #a6adc8);
|
|
479
|
+
word-break: break-word;
|
|
480
|
+
line-height: 1.45;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.lv__entry--error .lv__entry-message { color: var(--si-danger, #f38ba8); }
|
|
484
|
+
.lv__entry--warn .lv__entry-message { color: var(--si-warning, #f9e2af); }
|
|
485
|
+
|
|
486
|
+
.lv__entry-error {
|
|
487
|
+
display: none;
|
|
488
|
+
margin-top: 5px;
|
|
489
|
+
padding: 6px 8px;
|
|
490
|
+
border-radius: 6px;
|
|
491
|
+
background: color-mix(in srgb, var(--si-surface, #1e1e2e) 60%, #000);
|
|
492
|
+
font-size: 10px;
|
|
493
|
+
color: color-mix(in srgb, var(--si-dim, #a6adc8) 80%, transparent);
|
|
494
|
+
white-space: pre-wrap;
|
|
495
|
+
word-break: break-all;
|
|
496
|
+
font-family: inherit;
|
|
497
|
+
line-height: 1.5;
|
|
498
|
+
max-height: 180px;
|
|
499
|
+
overflow-y: auto;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.lv__entry[data-expanded] .lv__entry-error { display: block; }
|
|
503
|
+
|
|
504
|
+
.lv__empty {
|
|
505
|
+
display: flex;
|
|
506
|
+
flex-direction: column;
|
|
507
|
+
align-items: center;
|
|
508
|
+
justify-content: center;
|
|
509
|
+
height: 100%;
|
|
510
|
+
gap: 6px;
|
|
511
|
+
color: color-mix(in srgb, var(--si-dim, #a6adc8) 40%, transparent);
|
|
512
|
+
font-size: 12px;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.lv__empty-icon {
|
|
516
|
+
font-size: 24px;
|
|
517
|
+
opacity: .35;
|
|
518
|
+
line-height: 1;
|
|
519
|
+
}
|
|
520
|
+
`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function productionOnlyHtml() {
|
|
524
|
+
return `<div class="lv">
|
|
525
|
+
<div class="lv__header">
|
|
526
|
+
<div class="lv__header-left">
|
|
527
|
+
<span class="lv__title">Log Console</span>
|
|
528
|
+
<span class="lv__count" data-count></span>
|
|
529
|
+
</div>
|
|
530
|
+
<div class="lv__header-actions">
|
|
531
|
+
<button class="lv__filter" data-filter="error" title="Show errors only">Error</button>
|
|
532
|
+
<button class="lv__filter" data-filter="warn" title="Show warnings only">Warn</button>
|
|
533
|
+
<button class="lv__filter" data-filter="info" title="Show info only">Info</button>
|
|
534
|
+
<button class="lv__filter" data-filter="debug" title="Show debug only">Debug</button>
|
|
535
|
+
<button class="lv__clear" title="Clear all logs">Clear</button>
|
|
536
|
+
<button class="lv__close" title="Close">×</button>
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
<div class="lv__body" data-body></div>
|
|
540
|
+
</div>`;
|
|
541
|
+
}
|
|
@@ -10,6 +10,7 @@ const logTypes = {
|
|
|
10
10
|
export default class Logger {
|
|
11
11
|
constructor() {
|
|
12
12
|
this.logs = [];
|
|
13
|
+
this._logListeners = new Set();
|
|
13
14
|
this.logEnabled = slice.loggerConfig.enabled;
|
|
14
15
|
this.showLogsConfig = slice.loggerConfig.showLogs;
|
|
15
16
|
|
|
@@ -92,6 +93,9 @@ export default class Logger {
|
|
|
92
93
|
const log = new Log(logType, componentCategory, componentSliceId, message, error);
|
|
93
94
|
this.logs.push(log);
|
|
94
95
|
this.showLog(log);
|
|
96
|
+
for (const listener of this._logListeners) {
|
|
97
|
+
try { listener(log); } catch (_) { }
|
|
98
|
+
}
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
/**
|
|
@@ -155,6 +159,27 @@ export default class Logger {
|
|
|
155
159
|
this.info(componentSliceId, message, error);
|
|
156
160
|
}
|
|
157
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Subscribe to new log entries in real time.
|
|
164
|
+
* @param {Function} callback — receives the Log instance
|
|
165
|
+
* @returns {Function} the same callback (for passing to offLog)
|
|
166
|
+
*/
|
|
167
|
+
onLog(callback) {
|
|
168
|
+
if (typeof callback === 'function') {
|
|
169
|
+
this._logListeners.add(callback);
|
|
170
|
+
}
|
|
171
|
+
return callback;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Unsubscribe a previously registered listener.
|
|
176
|
+
* @param {Function} callback
|
|
177
|
+
* @returns {void}
|
|
178
|
+
*/
|
|
179
|
+
offLog(callback) {
|
|
180
|
+
this._logListeners.delete(callback);
|
|
181
|
+
}
|
|
182
|
+
|
|
158
183
|
/**
|
|
159
184
|
* Get all logs.
|
|
160
185
|
* @returns {Array}
|
package/Slice/Slice.js
CHANGED
|
@@ -564,9 +564,23 @@ async function init() {
|
|
|
564
564
|
window.slice.logger.logError('Slice', 'ContextManager disabled');
|
|
565
565
|
}
|
|
566
566
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
567
|
+
if (sliceConfig.logger?.ui?.enabled) {
|
|
568
|
+
try {
|
|
569
|
+
const LogViewerModule = window.slice.frameworkClasses?.LogViewer
|
|
570
|
+
|| await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Logger/LogViewer/LogViewer.js`);
|
|
571
|
+
const logViewer = new LogViewerModule();
|
|
572
|
+
window.slice.logViewer = logViewer;
|
|
573
|
+
logViewer.style.display = 'none';
|
|
574
|
+
document.body.appendChild(logViewer);
|
|
575
|
+
if (typeof logViewer.init === 'function') logViewer.init();
|
|
576
|
+
} catch (e) {
|
|
577
|
+
window.slice.logger?.warn?.('Slice', 'Could not load LogViewer component', e);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (sliceConfig.loading.enabled) {
|
|
582
|
+
const loading = await window.slice.build('Loading', {});
|
|
583
|
+
window.slice.loading = loading;
|
|
570
584
|
if (typeof loading?.start === 'function') {
|
|
571
585
|
loading.start();
|
|
572
586
|
}
|
|
@@ -575,7 +589,7 @@ async function init() {
|
|
|
575
589
|
const stylesInitPromise = window.slice.stylesManager.init();
|
|
576
590
|
const routesModulePromise = import(slice.paths.routesFile);
|
|
577
591
|
|
|
578
|
-
if (sliceConfig.events?.ui?.shortcut || sliceConfig.context?.ui?.shortcut) {
|
|
592
|
+
if (sliceConfig.events?.ui?.shortcut || sliceConfig.context?.ui?.shortcut || sliceConfig.logger?.ui?.shortcut) {
|
|
579
593
|
const normalize = (value) => (typeof value === 'string' ? value.toLowerCase() : '');
|
|
580
594
|
const toKey = (event) => {
|
|
581
595
|
const parts = [];
|
|
@@ -593,6 +607,7 @@ async function init() {
|
|
|
593
607
|
const handlers = {
|
|
594
608
|
[normalize(sliceConfig.events?.ui?.shortcut)]: () => window.slice.eventsDebugger?.toggle?.(),
|
|
595
609
|
[normalize(sliceConfig.context?.ui?.shortcut)]: () => window.slice.contextDebugger?.toggle?.(),
|
|
610
|
+
[normalize(sliceConfig.logger?.ui?.shortcut)]: () => window.slice.logViewer?.toggle?.(),
|
|
596
611
|
};
|
|
597
612
|
|
|
598
613
|
document.addEventListener('keydown', (event) => {
|