slicejs-web-framework 4.0.2 → 4.1.1

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,662 +1,662 @@
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 '&amp;';
17
- if (m === '<') return '&lt;';
18
- if (m === '>') return '&gt;';
19
- if (m === '"') return '&quot;';
20
- return '&#39;';
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
- this.$componentSelect = this.querySelector('[data-component-select]');
35
- this.$queryInput = this.querySelector('[data-query-input]');
36
-
37
- this._isOpen = false;
38
- this._logCount = 0;
39
- this._filters = new Set();
40
- this._componentFilter = '';
41
- this._queryFilter = '';
42
- this._dragState = null;
43
- this._logHandler = null;
44
- }
45
-
46
- init() {
47
- this._attachDrag();
48
- this._attachFilterClicks();
49
- this._attachClear();
50
- this._attachMinimize();
51
- this._attachBodyClick();
52
- this._attachComponentSelect();
53
- this._attachQueryInput();
54
- this._render();
55
- this._subscribeLogger();
56
- }
57
-
58
- toggle() {
59
- this._isOpen = !this._isOpen;
60
- this.style.display = this._isOpen ? '' : 'none';
61
- if (this._isOpen) {
62
- this._subscribeLogger();
63
- this._render();
64
- } else {
65
- this._unsubscribeLogger();
66
- }
67
- return this._isOpen;
68
- }
69
-
70
- _detach() {
71
- this._unsubscribeLogger();
72
- if (this._dragCleanup) this._dragCleanup();
73
- }
74
-
75
- /* -- drag -- */
76
- _attachDrag() {
77
- const header = this.$header;
78
- if (!header) return;
79
-
80
- const onDown = (e) => {
81
- if (e.target.closest('button')) return;
82
- this._dragState = {
83
- startX: e.clientX,
84
- startY: e.clientY,
85
- origLeft: this.offsetLeft,
86
- origTop: this.offsetTop,
87
- origRight: this.style.right,
88
- origBottom: this.style.bottom
89
- };
90
- this.style.right = 'auto';
91
- this.style.bottom = 'auto';
92
- document.addEventListener('mousemove', onMove);
93
- document.addEventListener('mouseup', onUp);
94
- };
95
-
96
- const onMove = (e) => {
97
- if (!this._dragState) return;
98
- const dx = e.clientX - this._dragState.startX;
99
- const dy = e.clientY - this._dragState.startY;
100
- this.style.left = (this._dragState.origLeft + dx) + 'px';
101
- this.style.top = (this._dragState.origTop + dy) + 'px';
102
- };
103
-
104
- const onUp = () => {
105
- this._dragState = null;
106
- document.removeEventListener('mousemove', onMove);
107
- document.removeEventListener('mouseup', onUp);
108
- };
109
-
110
- header.addEventListener('mousedown', onDown);
111
- this._dragCleanup = () => {
112
- header.removeEventListener('mousedown', onDown);
113
- document.removeEventListener('mousemove', onMove);
114
- document.removeEventListener('mouseup', onUp);
115
- };
116
- }
117
-
118
- /* -- filter buttons -- */
119
- _attachFilterClicks() {
120
- const btns = this.querySelectorAll('.lv__filter');
121
- for (const btn of btns) {
122
- btn.addEventListener('click', () => {
123
- const type = btn.dataset.filter;
124
- if (btn.hasAttribute('data-active')) {
125
- btn.removeAttribute('data-active');
126
- this._filters.delete(type);
127
- } else {
128
- btn.setAttribute('data-active', '');
129
- this._filters.add(type);
130
- }
131
- this._render();
132
- });
133
- }
134
- }
135
-
136
- /* -- clear -- */
137
- _attachClear() {
138
- const btn = this.querySelector('.lv__clear');
139
- if (!btn) return;
140
- btn.addEventListener('click', () => {
141
- if (slice.logger) slice.logger.logs = [];
142
- this._logCount = 0;
143
- this._render();
144
- });
145
- }
146
-
147
- /* -- minimize -- */
148
- _attachMinimize() {
149
- const btn = this.querySelector('.lv__close');
150
- if (!btn) return;
151
- btn.addEventListener('click', () => {
152
- if (this.hasAttribute('data-minimized')) {
153
- this.removeAttribute('data-minimized');
154
- btn.textContent = '\u00d7';
155
- } else {
156
- this.setAttribute('data-minimized', '');
157
- btn.textContent = '\u25a1';
158
- }
159
- });
160
- }
161
-
162
- /* -- expand error detail on click -- */
163
- _attachBodyClick() {
164
- if (!this.$body) return;
165
- this.$body.addEventListener('click', (e) => {
166
- const entry = e.target.closest('.lv__entry');
167
- if (!entry) return;
168
- const hasErr = entry.querySelector('.lv__entry-error');
169
- if (!hasErr) return;
170
- entry.toggleAttribute('data-expanded');
171
- });
172
- }
173
-
174
- /* -- component select -- */
175
- _attachComponentSelect() {
176
- if (!this.$componentSelect) return;
177
- this.$componentSelect.addEventListener('change', () => {
178
- this._componentFilter = this.$componentSelect.value;
179
- this._render();
180
- });
181
- }
182
-
183
- /* -- query text input (debounced) -- */
184
- _attachQueryInput() {
185
- if (!this.$queryInput) return;
186
- let timer;
187
- this.$queryInput.addEventListener('input', () => {
188
- clearTimeout(timer);
189
- timer = setTimeout(() => {
190
- this._queryFilter = this.$queryInput.value.trim().toLowerCase();
191
- this._render();
192
- }, 150);
193
- });
194
- }
195
-
196
- /* -- real-time subscription via logger.onLog -- */
197
- _subscribeLogger() {
198
- const logger = slice.logger;
199
- if (!logger || typeof logger.onLog !== 'function') return;
200
- this._logHandler = logger.onLog(() => {
201
- this._render();
202
- });
203
- }
204
-
205
- _unsubscribeLogger() {
206
- const logger = slice.logger;
207
- if (!logger || typeof logger.offLog !== 'function') return;
208
- if (this._logHandler) {
209
- logger.offLog(this._logHandler);
210
- this._logHandler = null;
211
- }
212
- }
213
-
214
- /* -- render -- */
215
- _render() {
216
- const logger = slice.logger;
217
- if (!logger || !logger.logs) return;
218
- const logs = logger.logs;
219
- this._logCount = logs.length;
220
-
221
- const filters = this._filters;
222
- const hasFilters = filters.size > 0;
223
- const hasComponent = this._componentFilter !== '';
224
- const hasQuery = this._queryFilter !== '';
225
-
226
- if (this.$count) {
227
- this.$count.textContent = logs.length;
228
- }
229
-
230
- /* refresh component select options */
231
- if (this.$componentSelect) {
232
- const currentVal = this.$componentSelect.value;
233
- const components = [...new Set(logs.map(l => l.componentSliceId).filter(Boolean))].sort();
234
- let opts = '<option value="">All components</option>';
235
- for (const c of components) {
236
- const sel = c === currentVal ? ' selected' : '';
237
- opts += `<option value="${escapeHtml(c)}"${sel}>${escapeHtml(c)}</option>`;
238
- }
239
- this.$componentSelect.innerHTML = opts;
240
- }
241
-
242
- if (!this.$body) return;
243
-
244
- if (logs.length === 0) {
245
- this.$body.innerHTML = '<div class="lv__empty"><span class="lv__empty-icon">&#128225;</span><span>No logs yet</span></div>';
246
- return;
247
- }
248
-
249
- let filtered = logs;
250
- if (hasFilters) filtered = filtered.filter((log) => filters.has(log.logType));
251
- if (hasComponent) filtered = filtered.filter((log) => log.componentSliceId === this._componentFilter);
252
- if (hasQuery) filtered = filtered.filter((log) => (log.message || '').toLowerCase().includes(this._queryFilter));
253
- const reversed = [...filtered].reverse();
254
-
255
- if (reversed.length === 0) {
256
- this.$body.innerHTML = '<div class="lv__empty"><span class="lv__empty-icon">&#128269;</span><span>No matches</span></div>';
257
- return;
258
- }
259
-
260
- const prevScrollTop = this.$body.scrollTop;
261
-
262
- let html = '';
263
- for (const log of reversed) {
264
- const type = log.logType || 'info';
265
- const ts = formatTime(log.timestamp instanceof Date ? log.timestamp : new Date(log.timestamp));
266
- const badge = LV_BADGE_LABEL[type] || 'INF';
267
- const component = escapeHtml(log.componentSliceId || '');
268
- const msg = escapeHtml(log.message || '');
269
-
270
- let errHtml = '';
271
- if (log.error) {
272
- const stack = log.error.stack || String(log.error);
273
- errHtml = `<div class="lv__entry-error">${escapeHtml(stack)}</div>`;
274
- }
275
-
276
- html += `<div class="lv__entry lv__entry--${type}">
277
- <span class="lv__entry-time">${ts}</span>
278
- <span class="lv__entry-badge">${badge}</span>
279
- <div class="lv__entry-body">
280
- <div class="lv__entry-component">${component}</div>
281
- <div class="lv__entry-message">${msg}</div>
282
- ${errHtml}
283
- </div>
284
- </div>`;
285
- }
286
-
287
- this.$body.innerHTML = html;
288
- /* preserve scroll — if user was at top (newest), stay at top */
289
- this.$body.scrollTop = prevScrollTop === 0 ? 0 : Math.min(prevScrollTop, this.$body.scrollHeight);
290
- }
291
- }
292
-
293
- customElements.define('slice-log-viewer', LogViewer);
294
-
295
- function productionOnlyCSS() {
296
- return `/* ── LogViewer (Structural) ── */
297
- slice-log-viewer {
298
- --si-accent: var(--primary-color, #6ee7ff);
299
- --si-accent-rgb: var(--primary-color-rgb, 110, 231, 255);
300
- --si-surface: var(--primary-background-color, #1e1e2e);
301
- --si-raised: var(--secondary-background-color, rgba(255, 255, 255, 0.035));
302
- --si-raised-2: var(--tertiary-background-color, #2a2a3e);
303
- --si-border: var(--medium-color, #555);
304
- --si-text: var(--font-primary-color, #cdd6f4);
305
- --si-dim: var(--font-secondary-color, #a6adc8);
306
- --si-danger: var(--danger-color, #f38ba8);
307
- --si-warning: var(--warning-color, #f9e2af);
308
- --si-info: var(--secondary-color, #89dceb);
309
- --si-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
310
- position: fixed;
311
- z-index: 2147483647;
312
- bottom: 24px;
313
- right: 24px;
314
- width: 520px;
315
- max-width: calc(100vw - 48px);
316
- height: 380px;
317
- max-height: calc(100vh - 48px);
318
- display: block;
319
- font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
320
- font-size: 12px;
321
- line-height: 1.5;
322
- border: 1px solid color-mix(in srgb, var(--si-border, #555) 60%, transparent);
323
- border-radius: 12px;
324
- background: color-mix(in srgb, var(--si-surface, #1e1e2e) 97%, #000);
325
- box-shadow: 0 12px 48px rgba(0,0,0,.45), 0 0 0 1px rgba(0,0,0,.08);
326
- overflow: hidden;
327
- user-select: none;
328
- resize: both;
329
- min-width: 280px;
330
- min-height: 200px;
331
- transition: opacity .18s ease, transform .18s ease;
332
- }
333
-
334
- slice-log-viewer[data-minimized] {
335
- height: auto !important;
336
- resize: none;
337
- min-height: 0;
338
- }
339
-
340
- slice-log-viewer[data-minimized] .lv__toolbar,
341
- slice-log-viewer[data-minimized] .lv__body {
342
- display: none;
343
- }
344
-
345
- .lv__header {
346
- display: flex;
347
- align-items: center;
348
- justify-content: space-between;
349
- gap: 8px;
350
- padding: 8px 10px 8px 14px;
351
- background: color-mix(in srgb, var(--si-raised-2, #2a2a3e) 80%, #000);
352
- border-bottom: 1px solid color-mix(in srgb, var(--si-border, #555) 40%, transparent);
353
- cursor: grab;
354
- position: relative;
355
- z-index: 1;
356
- }
357
-
358
- .lv__header:active { cursor: grabbing; }
359
-
360
- .lv__header-left {
361
- display: flex;
362
- align-items: center;
363
- gap: 8px;
364
- min-width: 0;
365
- }
366
-
367
- .lv__title {
368
- font-size: 13px;
369
- font-weight: 700;
370
- letter-spacing: .02em;
371
- color: var(--si-text, #cdd6f4);
372
- white-space: nowrap;
373
- }
374
-
375
- .lv__count {
376
- font-size: 10px;
377
- font-weight: 600;
378
- padding: 1px 6px;
379
- border-radius: 6px;
380
- background: color-mix(in srgb, var(--si-border, #555) 40%, transparent);
381
- color: var(--si-dim, #a6adc8);
382
- font-variant-numeric: tabular-nums;
383
- }
384
-
385
- .lv__header-actions {
386
- display: flex;
387
- align-items: center;
388
- gap: 4px;
389
- flex-shrink: 0;
390
- }
391
-
392
- .lv__filter,
393
- .lv__clear,
394
- .lv__close {
395
- all: unset;
396
- cursor: pointer;
397
- font-size: 10px;
398
- font-weight: 600;
399
- padding: 3px 7px;
400
- border-radius: 6px;
401
- color: var(--si-dim, #a6adc8);
402
- background: color-mix(in srgb, var(--si-border, #555) 25%, transparent);
403
- transition: background .12s ease, color .12s ease;
404
- font-family: inherit;
405
- line-height: 1.4;
406
- }
407
-
408
- .lv__filter:hover,
409
- .lv__clear:hover,
410
- .lv__close:hover {
411
- background: color-mix(in srgb, var(--si-border, #555) 45%, transparent);
412
- color: var(--si-text, #cdd6f4);
413
- }
414
-
415
- .lv__filter[data-active] { color: var(--si-text, #fff); }
416
-
417
- .lv__filter[data-active][data-filter="error"] {
418
- background: var(--si-danger, #f38ba8);
419
- color: var(--danger-contrast, #1e1e2e);
420
- }
421
-
422
- .lv__filter[data-active][data-filter="warn"] {
423
- background: var(--si-warning, #f9e2af);
424
- color: var(--warning-contrast, #1e1e2e);
425
- }
426
-
427
- .lv__filter[data-active][data-filter="info"] {
428
- background: var(--si-info, #89dceb);
429
- color: var(--secondary-color-contrast, #1e1e2e);
430
- }
431
-
432
- .lv__filter[data-active][data-filter="debug"] {
433
- background: var(--si-dim, #a6adc8);
434
- color: var(--si-text, #1e1e2e);
435
- }
436
-
437
- .lv__clear { margin-left: 2px; }
438
-
439
- .lv__close {
440
- font-size: 16px;
441
- line-height: 1;
442
- padding: 3px 8px 4px;
443
- }
444
-
445
- .lv__toolbar {
446
- display: flex;
447
- align-items: center;
448
- gap: 6px;
449
- padding: 5px 10px;
450
- background: color-mix(in srgb, var(--si-surface, #1e1e2e) 98%, #000);
451
- border-bottom: 1px solid color-mix(in srgb, var(--si-border, #555) 18%, transparent);
452
- }
453
-
454
- .lv__component-select {
455
- all: unset;
456
- flex: 0 0 auto;
457
- max-width: 160px;
458
- font-size: 10px;
459
- font-weight: 600;
460
- font-family: inherit;
461
- color: var(--si-text, #cdd6f4);
462
- background: color-mix(in srgb, var(--si-border, #555) 20%, transparent);
463
- padding: 3px 20px 3px 7px;
464
- border-radius: 6px;
465
- cursor: pointer;
466
- white-space: nowrap;
467
- overflow: hidden;
468
- text-overflow: ellipsis;
469
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cpath fill='%23a6adc8' d='M2 2.5l2 2 2-2'/%3E%3C/svg%3E");
470
- background-repeat: no-repeat;
471
- background-position: right 6px center;
472
- appearance: none;
473
- -webkit-appearance: none;
474
- }
475
-
476
- .lv__component-select:hover {
477
- background-color: color-mix(in srgb, var(--si-border, #555) 35%, transparent);
478
- }
479
-
480
- .lv__component-select option {
481
- background: var(--si-surface, #1e1e2e);
482
- color: var(--si-text, #cdd6f4);
483
- font-size: 10px;
484
- }
485
-
486
- .lv__query-input {
487
- all: unset;
488
- flex: 1;
489
- min-width: 0;
490
- font-size: 10px;
491
- font-family: inherit;
492
- color: var(--si-text, #cdd6f4);
493
- background: color-mix(in srgb, var(--si-border, #555) 15%, transparent);
494
- padding: 3px 7px;
495
- border-radius: 6px;
496
- transition: background .12s ease;
497
- }
498
-
499
- .lv__query-input::placeholder {
500
- color: color-mix(in srgb, var(--si-dim, #a6adc8) 45%, transparent);
501
- }
502
-
503
- .lv__query-input:focus {
504
- background: color-mix(in srgb, var(--si-border, #555) 25%, transparent);
505
- outline: 1px solid color-mix(in srgb, var(--si-accent, #6ee7ff) 30%, transparent);
506
- }
507
-
508
- .lv__body {
509
- overflow-y: auto;
510
- overflow-x: hidden;
511
- height: calc(100% - 76px);
512
- padding: 4px 0;
513
- cursor: auto;
514
- }
515
-
516
- .lv__body::-webkit-scrollbar { width: 5px; }
517
- .lv__body::-webkit-scrollbar-track { background: transparent; }
518
- .lv__body::-webkit-scrollbar-thumb {
519
- background: color-mix(in srgb, var(--si-border, #555) 40%, transparent);
520
- border-radius: 3px;
521
- }
522
-
523
- .lv__entry {
524
- display: flex;
525
- align-items: flex-start;
526
- gap: 8px;
527
- padding: 5px 14px;
528
- border-bottom: 1px solid color-mix(in srgb, var(--si-border, #555) 12%, transparent);
529
- transition: background .1s ease;
530
- cursor: pointer;
531
- }
532
-
533
- .lv__entry:hover {
534
- background: color-mix(in srgb, var(--si-border, #555) 8%, transparent);
535
- }
536
-
537
- .lv__entry:last-child { border-bottom: none; }
538
-
539
- .lv__entry-time {
540
- font-size: 10px;
541
- color: color-mix(in srgb, var(--si-dim, #a6adc8) 55%, transparent);
542
- white-space: nowrap;
543
- flex-shrink: 0;
544
- font-variant-numeric: tabular-nums;
545
- margin-top: 1px;
546
- }
547
-
548
- .lv__entry-badge {
549
- font-size: 9px;
550
- font-weight: 700;
551
- padding: 1px 5px;
552
- border-radius: 4px;
553
- text-transform: uppercase;
554
- letter-spacing: .04em;
555
- flex-shrink: 0;
556
- margin-top: 1px;
557
- }
558
-
559
- .lv__entry--error .lv__entry-badge {
560
- background: var(--si-danger, #f38ba8);
561
- color: var(--danger-contrast, #1e1e2e);
562
- }
563
-
564
- .lv__entry--warn .lv__entry-badge {
565
- background: var(--si-warning, #f9e2af);
566
- color: var(--warning-contrast, #1e1e2e);
567
- }
568
-
569
- .lv__entry--info .lv__entry-badge {
570
- background: var(--si-info, #89dceb);
571
- color: var(--secondary-color-contrast, #1e1e2e);
572
- }
573
-
574
- .lv__entry--debug .lv__entry-badge {
575
- background: color-mix(in srgb, var(--si-border, #555) 40%, transparent);
576
- color: var(--si-dim, #a6adc8);
577
- }
578
-
579
- .lv__entry-body {
580
- flex: 1;
581
- min-width: 0;
582
- }
583
-
584
- .lv__entry-component {
585
- font-size: 10px;
586
- font-weight: 600;
587
- color: var(--si-text, #cdd6f4);
588
- margin-bottom: 1px;
589
- }
590
-
591
- .lv__entry-message {
592
- font-size: 11px;
593
- color: var(--si-dim, #a6adc8);
594
- word-break: break-word;
595
- line-height: 1.45;
596
- }
597
-
598
- .lv__entry--error .lv__entry-message { color: var(--si-danger, #f38ba8); }
599
- .lv__entry--warn .lv__entry-message { color: var(--si-warning, #f9e2af); }
600
-
601
- .lv__entry-error {
602
- display: none;
603
- margin-top: 5px;
604
- padding: 6px 8px;
605
- border-radius: 6px;
606
- background: color-mix(in srgb, var(--si-surface, #1e1e2e) 60%, #000);
607
- font-size: 10px;
608
- color: color-mix(in srgb, var(--si-dim, #a6adc8) 80%, transparent);
609
- white-space: pre-wrap;
610
- word-break: break-all;
611
- font-family: inherit;
612
- line-height: 1.5;
613
- max-height: 180px;
614
- overflow-y: auto;
615
- }
616
-
617
- .lv__entry[data-expanded] .lv__entry-error { display: block; }
618
-
619
- .lv__empty {
620
- display: flex;
621
- flex-direction: column;
622
- align-items: center;
623
- justify-content: center;
624
- height: 100%;
625
- gap: 6px;
626
- color: color-mix(in srgb, var(--si-dim, #a6adc8) 40%, transparent);
627
- font-size: 12px;
628
- }
629
-
630
- .lv__empty-icon {
631
- font-size: 24px;
632
- opacity: .35;
633
- line-height: 1;
634
- }
635
- `;
636
- }
637
-
638
- function productionOnlyHtml() {
639
- return `<div class="lv">
640
- <div class="lv__header">
641
- <div class="lv__header-left">
642
- <span class="lv__title">Log Console</span>
643
- <span class="lv__count" data-count></span>
644
- </div>
645
- <div class="lv__header-actions">
646
- <button class="lv__filter" data-filter="error" title="Show errors only">Error</button>
647
- <button class="lv__filter" data-filter="warn" title="Show warnings only">Warn</button>
648
- <button class="lv__filter" data-filter="info" title="Show info only">Info</button>
649
- <button class="lv__filter" data-filter="debug" title="Show debug only">Debug</button>
650
- <button class="lv__clear" title="Clear all logs">Clear</button>
651
- <button class="lv__close" title="Close">&times;</button>
652
- </div>
653
- </div>
654
- <div class="lv__toolbar">
655
- <select class="lv__component-select" data-component-select>
656
- <option value="">All components</option>
657
- </select>
658
- <input class="lv__query-input" data-query-input type="text" placeholder="Search logs..." />
659
- </div>
660
- <div class="lv__body" data-body></div>
661
- </div>`;
662
- }
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 '&amp;';
17
+ if (m === '<') return '&lt;';
18
+ if (m === '>') return '&gt;';
19
+ if (m === '"') return '&quot;';
20
+ return '&#39;';
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
+ this.$componentSelect = this.querySelector('[data-component-select]');
35
+ this.$queryInput = this.querySelector('[data-query-input]');
36
+
37
+ this._isOpen = false;
38
+ this._logCount = 0;
39
+ this._filters = new Set();
40
+ this._componentFilter = '';
41
+ this._queryFilter = '';
42
+ this._dragState = null;
43
+ this._logHandler = null;
44
+ }
45
+
46
+ init() {
47
+ this._attachDrag();
48
+ this._attachFilterClicks();
49
+ this._attachClear();
50
+ this._attachMinimize();
51
+ this._attachBodyClick();
52
+ this._attachComponentSelect();
53
+ this._attachQueryInput();
54
+ this._render();
55
+ this._subscribeLogger();
56
+ }
57
+
58
+ toggle() {
59
+ this._isOpen = !this._isOpen;
60
+ this.style.display = this._isOpen ? '' : 'none';
61
+ if (this._isOpen) {
62
+ this._subscribeLogger();
63
+ this._render();
64
+ } else {
65
+ this._unsubscribeLogger();
66
+ }
67
+ return this._isOpen;
68
+ }
69
+
70
+ _detach() {
71
+ this._unsubscribeLogger();
72
+ if (this._dragCleanup) this._dragCleanup();
73
+ }
74
+
75
+ /* -- drag -- */
76
+ _attachDrag() {
77
+ const header = this.$header;
78
+ if (!header) return;
79
+
80
+ const onDown = (e) => {
81
+ if (e.target.closest('button')) return;
82
+ this._dragState = {
83
+ startX: e.clientX,
84
+ startY: e.clientY,
85
+ origLeft: this.offsetLeft,
86
+ origTop: this.offsetTop,
87
+ origRight: this.style.right,
88
+ origBottom: this.style.bottom
89
+ };
90
+ this.style.right = 'auto';
91
+ this.style.bottom = 'auto';
92
+ document.addEventListener('mousemove', onMove);
93
+ document.addEventListener('mouseup', onUp);
94
+ };
95
+
96
+ const onMove = (e) => {
97
+ if (!this._dragState) return;
98
+ const dx = e.clientX - this._dragState.startX;
99
+ const dy = e.clientY - this._dragState.startY;
100
+ this.style.left = (this._dragState.origLeft + dx) + 'px';
101
+ this.style.top = (this._dragState.origTop + dy) + 'px';
102
+ };
103
+
104
+ const onUp = () => {
105
+ this._dragState = null;
106
+ document.removeEventListener('mousemove', onMove);
107
+ document.removeEventListener('mouseup', onUp);
108
+ };
109
+
110
+ header.addEventListener('mousedown', onDown);
111
+ this._dragCleanup = () => {
112
+ header.removeEventListener('mousedown', onDown);
113
+ document.removeEventListener('mousemove', onMove);
114
+ document.removeEventListener('mouseup', onUp);
115
+ };
116
+ }
117
+
118
+ /* -- filter buttons -- */
119
+ _attachFilterClicks() {
120
+ const btns = this.querySelectorAll('.lv__filter');
121
+ for (const btn of btns) {
122
+ btn.addEventListener('click', () => {
123
+ const type = btn.dataset.filter;
124
+ if (btn.hasAttribute('data-active')) {
125
+ btn.removeAttribute('data-active');
126
+ this._filters.delete(type);
127
+ } else {
128
+ btn.setAttribute('data-active', '');
129
+ this._filters.add(type);
130
+ }
131
+ this._render();
132
+ });
133
+ }
134
+ }
135
+
136
+ /* -- clear -- */
137
+ _attachClear() {
138
+ const btn = this.querySelector('.lv__clear');
139
+ if (!btn) return;
140
+ btn.addEventListener('click', () => {
141
+ if (slice.logger) slice.logger.logs = [];
142
+ this._logCount = 0;
143
+ this._render();
144
+ });
145
+ }
146
+
147
+ /* -- minimize -- */
148
+ _attachMinimize() {
149
+ const btn = this.querySelector('.lv__close');
150
+ if (!btn) return;
151
+ btn.addEventListener('click', () => {
152
+ if (this.hasAttribute('data-minimized')) {
153
+ this.removeAttribute('data-minimized');
154
+ btn.textContent = '\u00d7';
155
+ } else {
156
+ this.setAttribute('data-minimized', '');
157
+ btn.textContent = '\u25a1';
158
+ }
159
+ });
160
+ }
161
+
162
+ /* -- expand error detail on click -- */
163
+ _attachBodyClick() {
164
+ if (!this.$body) return;
165
+ this.$body.addEventListener('click', (e) => {
166
+ const entry = e.target.closest('.lv__entry');
167
+ if (!entry) return;
168
+ const hasErr = entry.querySelector('.lv__entry-error');
169
+ if (!hasErr) return;
170
+ entry.toggleAttribute('data-expanded');
171
+ });
172
+ }
173
+
174
+ /* -- component select -- */
175
+ _attachComponentSelect() {
176
+ if (!this.$componentSelect) return;
177
+ this.$componentSelect.addEventListener('change', () => {
178
+ this._componentFilter = this.$componentSelect.value;
179
+ this._render();
180
+ });
181
+ }
182
+
183
+ /* -- query text input (debounced) -- */
184
+ _attachQueryInput() {
185
+ if (!this.$queryInput) return;
186
+ let timer;
187
+ this.$queryInput.addEventListener('input', () => {
188
+ clearTimeout(timer);
189
+ timer = setTimeout(() => {
190
+ this._queryFilter = this.$queryInput.value.trim().toLowerCase();
191
+ this._render();
192
+ }, 150);
193
+ });
194
+ }
195
+
196
+ /* -- real-time subscription via logger.onLog -- */
197
+ _subscribeLogger() {
198
+ const logger = slice.logger;
199
+ if (!logger || typeof logger.onLog !== 'function') return;
200
+ this._logHandler = logger.onLog(() => {
201
+ this._render();
202
+ });
203
+ }
204
+
205
+ _unsubscribeLogger() {
206
+ const logger = slice.logger;
207
+ if (!logger || typeof logger.offLog !== 'function') return;
208
+ if (this._logHandler) {
209
+ logger.offLog(this._logHandler);
210
+ this._logHandler = null;
211
+ }
212
+ }
213
+
214
+ /* -- render -- */
215
+ _render() {
216
+ const logger = slice.logger;
217
+ if (!logger || !logger.logs) return;
218
+ const logs = logger.logs;
219
+ this._logCount = logs.length;
220
+
221
+ const filters = this._filters;
222
+ const hasFilters = filters.size > 0;
223
+ const hasComponent = this._componentFilter !== '';
224
+ const hasQuery = this._queryFilter !== '';
225
+
226
+ if (this.$count) {
227
+ this.$count.textContent = logs.length;
228
+ }
229
+
230
+ /* refresh component select options */
231
+ if (this.$componentSelect) {
232
+ const currentVal = this.$componentSelect.value;
233
+ const components = [...new Set(logs.map(l => l.componentSliceId).filter(Boolean))].sort();
234
+ let opts = '<option value="">All components</option>';
235
+ for (const c of components) {
236
+ const sel = c === currentVal ? ' selected' : '';
237
+ opts += `<option value="${escapeHtml(c)}"${sel}>${escapeHtml(c)}</option>`;
238
+ }
239
+ this.$componentSelect.innerHTML = opts;
240
+ }
241
+
242
+ if (!this.$body) return;
243
+
244
+ if (logs.length === 0) {
245
+ this.$body.innerHTML = '<div class="lv__empty"><span class="lv__empty-icon">&#128225;</span><span>No logs yet</span></div>';
246
+ return;
247
+ }
248
+
249
+ let filtered = logs;
250
+ if (hasFilters) filtered = filtered.filter((log) => filters.has(log.logType));
251
+ if (hasComponent) filtered = filtered.filter((log) => log.componentSliceId === this._componentFilter);
252
+ if (hasQuery) filtered = filtered.filter((log) => (log.message || '').toLowerCase().includes(this._queryFilter));
253
+ const reversed = [...filtered].reverse();
254
+
255
+ if (reversed.length === 0) {
256
+ this.$body.innerHTML = '<div class="lv__empty"><span class="lv__empty-icon">&#128269;</span><span>No matches</span></div>';
257
+ return;
258
+ }
259
+
260
+ const prevScrollTop = this.$body.scrollTop;
261
+
262
+ let html = '';
263
+ for (const log of reversed) {
264
+ const type = log.logType || 'info';
265
+ const ts = formatTime(log.timestamp instanceof Date ? log.timestamp : new Date(log.timestamp));
266
+ const badge = LV_BADGE_LABEL[type] || 'INF';
267
+ const component = escapeHtml(log.componentSliceId || '');
268
+ const msg = escapeHtml(log.message || '');
269
+
270
+ let errHtml = '';
271
+ if (log.error) {
272
+ const stack = log.error.stack || String(log.error);
273
+ errHtml = `<div class="lv__entry-error">${escapeHtml(stack)}</div>`;
274
+ }
275
+
276
+ html += `<div class="lv__entry lv__entry--${type}">
277
+ <span class="lv__entry-time">${ts}</span>
278
+ <span class="lv__entry-badge">${badge}</span>
279
+ <div class="lv__entry-body">
280
+ <div class="lv__entry-component">${component}</div>
281
+ <div class="lv__entry-message">${msg}</div>
282
+ ${errHtml}
283
+ </div>
284
+ </div>`;
285
+ }
286
+
287
+ this.$body.innerHTML = html;
288
+ /* preserve scroll — if user was at top (newest), stay at top */
289
+ this.$body.scrollTop = prevScrollTop === 0 ? 0 : Math.min(prevScrollTop, this.$body.scrollHeight);
290
+ }
291
+ }
292
+
293
+ customElements.define('slice-log-viewer', LogViewer);
294
+
295
+ function productionOnlyCSS() {
296
+ return `/* ── LogViewer (Structural) ── */
297
+ slice-log-viewer {
298
+ --si-accent: var(--primary-color, #6ee7ff);
299
+ --si-accent-rgb: var(--primary-color-rgb, 110, 231, 255);
300
+ --si-surface: var(--primary-background-color, #1e1e2e);
301
+ --si-raised: var(--secondary-background-color, rgba(255, 255, 255, 0.035));
302
+ --si-raised-2: var(--tertiary-background-color, #2a2a3e);
303
+ --si-border: var(--medium-color, #555);
304
+ --si-text: var(--font-primary-color, #cdd6f4);
305
+ --si-dim: var(--font-secondary-color, #a6adc8);
306
+ --si-danger: var(--danger-color, #f38ba8);
307
+ --si-warning: var(--warning-color, #f9e2af);
308
+ --si-info: var(--secondary-color, #89dceb);
309
+ --si-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
310
+ position: fixed;
311
+ z-index: 2147483647;
312
+ bottom: 24px;
313
+ right: 24px;
314
+ width: 520px;
315
+ max-width: calc(100vw - 48px);
316
+ height: 380px;
317
+ max-height: calc(100vh - 48px);
318
+ display: block;
319
+ font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
320
+ font-size: 12px;
321
+ line-height: 1.5;
322
+ border: 1px solid color-mix(in srgb, var(--si-border, #555) 60%, transparent);
323
+ border-radius: 12px;
324
+ background: color-mix(in srgb, var(--si-surface, #1e1e2e) 97%, #000);
325
+ box-shadow: 0 12px 48px rgba(0,0,0,.45), 0 0 0 1px rgba(0,0,0,.08);
326
+ overflow: hidden;
327
+ user-select: none;
328
+ resize: both;
329
+ min-width: 280px;
330
+ min-height: 200px;
331
+ transition: opacity .18s ease, transform .18s ease;
332
+ }
333
+
334
+ slice-log-viewer[data-minimized] {
335
+ height: auto !important;
336
+ resize: none;
337
+ min-height: 0;
338
+ }
339
+
340
+ slice-log-viewer[data-minimized] .lv__toolbar,
341
+ slice-log-viewer[data-minimized] .lv__body {
342
+ display: none;
343
+ }
344
+
345
+ .lv__header {
346
+ display: flex;
347
+ align-items: center;
348
+ justify-content: space-between;
349
+ gap: 8px;
350
+ padding: 8px 10px 8px 14px;
351
+ background: color-mix(in srgb, var(--si-raised-2, #2a2a3e) 80%, #000);
352
+ border-bottom: 1px solid color-mix(in srgb, var(--si-border, #555) 40%, transparent);
353
+ cursor: grab;
354
+ position: relative;
355
+ z-index: 1;
356
+ }
357
+
358
+ .lv__header:active { cursor: grabbing; }
359
+
360
+ .lv__header-left {
361
+ display: flex;
362
+ align-items: center;
363
+ gap: 8px;
364
+ min-width: 0;
365
+ }
366
+
367
+ .lv__title {
368
+ font-size: 13px;
369
+ font-weight: 700;
370
+ letter-spacing: .02em;
371
+ color: var(--si-text, #cdd6f4);
372
+ white-space: nowrap;
373
+ }
374
+
375
+ .lv__count {
376
+ font-size: 10px;
377
+ font-weight: 600;
378
+ padding: 1px 6px;
379
+ border-radius: 6px;
380
+ background: color-mix(in srgb, var(--si-border, #555) 40%, transparent);
381
+ color: var(--si-dim, #a6adc8);
382
+ font-variant-numeric: tabular-nums;
383
+ }
384
+
385
+ .lv__header-actions {
386
+ display: flex;
387
+ align-items: center;
388
+ gap: 4px;
389
+ flex-shrink: 0;
390
+ }
391
+
392
+ .lv__filter,
393
+ .lv__clear,
394
+ .lv__close {
395
+ all: unset;
396
+ cursor: pointer;
397
+ font-size: 10px;
398
+ font-weight: 600;
399
+ padding: 3px 7px;
400
+ border-radius: 6px;
401
+ color: var(--si-dim, #a6adc8);
402
+ background: color-mix(in srgb, var(--si-border, #555) 25%, transparent);
403
+ transition: background .12s ease, color .12s ease;
404
+ font-family: inherit;
405
+ line-height: 1.4;
406
+ }
407
+
408
+ .lv__filter:hover,
409
+ .lv__clear:hover,
410
+ .lv__close:hover {
411
+ background: color-mix(in srgb, var(--si-border, #555) 45%, transparent);
412
+ color: var(--si-text, #cdd6f4);
413
+ }
414
+
415
+ .lv__filter[data-active] { color: var(--si-text, #fff); }
416
+
417
+ .lv__filter[data-active][data-filter="error"] {
418
+ background: var(--si-danger, #f38ba8);
419
+ color: var(--danger-contrast, #1e1e2e);
420
+ }
421
+
422
+ .lv__filter[data-active][data-filter="warn"] {
423
+ background: var(--si-warning, #f9e2af);
424
+ color: var(--warning-contrast, #1e1e2e);
425
+ }
426
+
427
+ .lv__filter[data-active][data-filter="info"] {
428
+ background: var(--si-info, #89dceb);
429
+ color: var(--secondary-color-contrast, #1e1e2e);
430
+ }
431
+
432
+ .lv__filter[data-active][data-filter="debug"] {
433
+ background: var(--si-dim, #a6adc8);
434
+ color: var(--si-text, #1e1e2e);
435
+ }
436
+
437
+ .lv__clear { margin-left: 2px; }
438
+
439
+ .lv__close {
440
+ font-size: 16px;
441
+ line-height: 1;
442
+ padding: 3px 8px 4px;
443
+ }
444
+
445
+ .lv__toolbar {
446
+ display: flex;
447
+ align-items: center;
448
+ gap: 6px;
449
+ padding: 5px 10px;
450
+ background: color-mix(in srgb, var(--si-surface, #1e1e2e) 98%, #000);
451
+ border-bottom: 1px solid color-mix(in srgb, var(--si-border, #555) 18%, transparent);
452
+ }
453
+
454
+ .lv__component-select {
455
+ all: unset;
456
+ flex: 0 0 auto;
457
+ max-width: 160px;
458
+ font-size: 10px;
459
+ font-weight: 600;
460
+ font-family: inherit;
461
+ color: var(--si-text, #cdd6f4);
462
+ background: color-mix(in srgb, var(--si-border, #555) 20%, transparent);
463
+ padding: 3px 20px 3px 7px;
464
+ border-radius: 6px;
465
+ cursor: pointer;
466
+ white-space: nowrap;
467
+ overflow: hidden;
468
+ text-overflow: ellipsis;
469
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cpath fill='%23a6adc8' d='M2 2.5l2 2 2-2'/%3E%3C/svg%3E");
470
+ background-repeat: no-repeat;
471
+ background-position: right 6px center;
472
+ appearance: none;
473
+ -webkit-appearance: none;
474
+ }
475
+
476
+ .lv__component-select:hover {
477
+ background-color: color-mix(in srgb, var(--si-border, #555) 35%, transparent);
478
+ }
479
+
480
+ .lv__component-select option {
481
+ background: var(--si-surface, #1e1e2e);
482
+ color: var(--si-text, #cdd6f4);
483
+ font-size: 10px;
484
+ }
485
+
486
+ .lv__query-input {
487
+ all: unset;
488
+ flex: 1;
489
+ min-width: 0;
490
+ font-size: 10px;
491
+ font-family: inherit;
492
+ color: var(--si-text, #cdd6f4);
493
+ background: color-mix(in srgb, var(--si-border, #555) 15%, transparent);
494
+ padding: 3px 7px;
495
+ border-radius: 6px;
496
+ transition: background .12s ease;
497
+ }
498
+
499
+ .lv__query-input::placeholder {
500
+ color: color-mix(in srgb, var(--si-dim, #a6adc8) 45%, transparent);
501
+ }
502
+
503
+ .lv__query-input:focus {
504
+ background: color-mix(in srgb, var(--si-border, #555) 25%, transparent);
505
+ outline: 1px solid color-mix(in srgb, var(--si-accent, #6ee7ff) 30%, transparent);
506
+ }
507
+
508
+ .lv__body {
509
+ overflow-y: auto;
510
+ overflow-x: hidden;
511
+ height: calc(100% - 76px);
512
+ padding: 4px 0;
513
+ cursor: auto;
514
+ }
515
+
516
+ .lv__body::-webkit-scrollbar { width: 5px; }
517
+ .lv__body::-webkit-scrollbar-track { background: transparent; }
518
+ .lv__body::-webkit-scrollbar-thumb {
519
+ background: color-mix(in srgb, var(--si-border, #555) 40%, transparent);
520
+ border-radius: 3px;
521
+ }
522
+
523
+ .lv__entry {
524
+ display: flex;
525
+ align-items: flex-start;
526
+ gap: 8px;
527
+ padding: 5px 14px;
528
+ border-bottom: 1px solid color-mix(in srgb, var(--si-border, #555) 12%, transparent);
529
+ transition: background .1s ease;
530
+ cursor: pointer;
531
+ }
532
+
533
+ .lv__entry:hover {
534
+ background: color-mix(in srgb, var(--si-border, #555) 8%, transparent);
535
+ }
536
+
537
+ .lv__entry:last-child { border-bottom: none; }
538
+
539
+ .lv__entry-time {
540
+ font-size: 10px;
541
+ color: color-mix(in srgb, var(--si-dim, #a6adc8) 55%, transparent);
542
+ white-space: nowrap;
543
+ flex-shrink: 0;
544
+ font-variant-numeric: tabular-nums;
545
+ margin-top: 1px;
546
+ }
547
+
548
+ .lv__entry-badge {
549
+ font-size: 9px;
550
+ font-weight: 700;
551
+ padding: 1px 5px;
552
+ border-radius: 4px;
553
+ text-transform: uppercase;
554
+ letter-spacing: .04em;
555
+ flex-shrink: 0;
556
+ margin-top: 1px;
557
+ }
558
+
559
+ .lv__entry--error .lv__entry-badge {
560
+ background: var(--si-danger, #f38ba8);
561
+ color: var(--danger-contrast, #1e1e2e);
562
+ }
563
+
564
+ .lv__entry--warn .lv__entry-badge {
565
+ background: var(--si-warning, #f9e2af);
566
+ color: var(--warning-contrast, #1e1e2e);
567
+ }
568
+
569
+ .lv__entry--info .lv__entry-badge {
570
+ background: var(--si-info, #89dceb);
571
+ color: var(--secondary-color-contrast, #1e1e2e);
572
+ }
573
+
574
+ .lv__entry--debug .lv__entry-badge {
575
+ background: color-mix(in srgb, var(--si-border, #555) 40%, transparent);
576
+ color: var(--si-dim, #a6adc8);
577
+ }
578
+
579
+ .lv__entry-body {
580
+ flex: 1;
581
+ min-width: 0;
582
+ }
583
+
584
+ .lv__entry-component {
585
+ font-size: 10px;
586
+ font-weight: 600;
587
+ color: var(--si-text, #cdd6f4);
588
+ margin-bottom: 1px;
589
+ }
590
+
591
+ .lv__entry-message {
592
+ font-size: 11px;
593
+ color: var(--si-dim, #a6adc8);
594
+ word-break: break-word;
595
+ line-height: 1.45;
596
+ }
597
+
598
+ .lv__entry--error .lv__entry-message { color: var(--si-danger, #f38ba8); }
599
+ .lv__entry--warn .lv__entry-message { color: var(--si-warning, #f9e2af); }
600
+
601
+ .lv__entry-error {
602
+ display: none;
603
+ margin-top: 5px;
604
+ padding: 6px 8px;
605
+ border-radius: 6px;
606
+ background: color-mix(in srgb, var(--si-surface, #1e1e2e) 60%, #000);
607
+ font-size: 10px;
608
+ color: color-mix(in srgb, var(--si-dim, #a6adc8) 80%, transparent);
609
+ white-space: pre-wrap;
610
+ word-break: break-all;
611
+ font-family: inherit;
612
+ line-height: 1.5;
613
+ max-height: 180px;
614
+ overflow-y: auto;
615
+ }
616
+
617
+ .lv__entry[data-expanded] .lv__entry-error { display: block; }
618
+
619
+ .lv__empty {
620
+ display: flex;
621
+ flex-direction: column;
622
+ align-items: center;
623
+ justify-content: center;
624
+ height: 100%;
625
+ gap: 6px;
626
+ color: color-mix(in srgb, var(--si-dim, #a6adc8) 40%, transparent);
627
+ font-size: 12px;
628
+ }
629
+
630
+ .lv__empty-icon {
631
+ font-size: 24px;
632
+ opacity: .35;
633
+ line-height: 1;
634
+ }
635
+ `;
636
+ }
637
+
638
+ function productionOnlyHtml() {
639
+ return `<div class="lv">
640
+ <div class="lv__header">
641
+ <div class="lv__header-left">
642
+ <span class="lv__title">Log Console</span>
643
+ <span class="lv__count" data-count></span>
644
+ </div>
645
+ <div class="lv__header-actions">
646
+ <button class="lv__filter" data-filter="error" title="Show errors only">Error</button>
647
+ <button class="lv__filter" data-filter="warn" title="Show warnings only">Warn</button>
648
+ <button class="lv__filter" data-filter="info" title="Show info only">Info</button>
649
+ <button class="lv__filter" data-filter="debug" title="Show debug only">Debug</button>
650
+ <button class="lv__clear" title="Clear all logs">Clear</button>
651
+ <button class="lv__close" title="Close">&times;</button>
652
+ </div>
653
+ </div>
654
+ <div class="lv__toolbar">
655
+ <select class="lv__component-select" data-component-select>
656
+ <option value="">All components</option>
657
+ </select>
658
+ <input class="lv__query-input" data-query-input type="text" placeholder="Search logs..." />
659
+ </div>
660
+ <div class="lv__body" data-body></div>
661
+ </div>`;
662
+ }