slicejs-web-framework 3.3.7 → 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.
@@ -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 '&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
+
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">&#128225;</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">&times;</button>
537
+ </div>
538
+ </div>
539
+ <div class="lv__body" data-body></div>
540
+ </div>`;
541
+ }