devlog-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexander Remer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,673 @@
1
+ # LogView
2
+
3
+ A lightweight, browser-based dev logger with a beautiful debug UI. Zero dependencies, framework-agnostic, production-safe.
4
+
5
+ ## Features
6
+
7
+ - 📋 **Structured Logging** - Replace `console.log` with type-safe, structured logs
8
+ - 🔍 **Source Location** - Automatic file, line, and function tracking
9
+ - 🎨 **Debug UI** - Shadow DOM overlay with filter, search, and pop-out window
10
+ - 🚀 **Zero Production Overhead** - Tree-shakeable no-op export for production builds
11
+ - 🔒 **Crash-Resistant** - Never throws, never breaks your app
12
+ - ⚡ **Global Error Capture** - Automatically catch uncaught errors and unhandled rejections
13
+ - 💾 **Persistence & Crash Recovery** - Survive page crashes with automatic log persistence
14
+ - 🎯 **Spans & Grouping** - Group related logs with timing and nested spans
15
+ - 🏷️ **Context & Tags** - Attach requestId, userId, or any context to logs
16
+ - 📤 **Export & Share** - Copy logs as JSON or text for bug reports
17
+ - 🔄 **Visual Diff** - Compare objects with color-coded change visualization
18
+ - 🌐 **Network Capture** - Automatic Fetch/XHR request tracking with spans
19
+ - 📊 **Timeline View** - Canvas-based visualization of logs and spans over time
20
+ - 🌐 **Framework-Agnostic** - Works with React, Vue, Svelte, vanilla JS, or any framework
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install devlogger
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ```typescript
31
+ import { logger, DevLoggerUI } from 'devlogger';
32
+
33
+ // Initialize the UI (once, at app start)
34
+ DevLoggerUI.init();
35
+
36
+ // Log messages with automatic source tracking
37
+ logger.info('App started');
38
+ logger.debug('Loading config', { theme: 'dark' });
39
+ logger.warn('Cache miss', { key: 'user_prefs' });
40
+ logger.error('API failed', new Error('Network timeout'));
41
+ ```
42
+
43
+ ## API Reference
44
+
45
+ ### Logger
46
+
47
+ The `logger` singleton provides four log levels:
48
+
49
+ ```typescript
50
+ // Debug - verbose development info
51
+ logger.debug(message: string, ...data: unknown[]): void
52
+
53
+ // Info - general information
54
+ logger.info(message: string, ...data: unknown[]): void
55
+
56
+ // Warning - potential issues
57
+ logger.warn(message: string, ...data: unknown[]): void
58
+
59
+ // Error - errors and exceptions
60
+ logger.error(message: string, ...data: unknown[]): void
61
+ ```
62
+
63
+ #### Configuration
64
+
65
+ ```typescript
66
+ logger.configure({
67
+ maxLogs: 1000, // Max logs in memory (FIFO rotation)
68
+ minLevel: 'debug', // Minimum level: 'debug' | 'info' | 'warn' | 'error'
69
+ enabled: true, // Enable/disable logging
70
+ });
71
+ ```
72
+
73
+ #### Other Methods
74
+
75
+ ```typescript
76
+ // Get all logs (readonly array)
77
+ const logs = logger.getLogs();
78
+
79
+ // Clear all logs
80
+ logger.clear();
81
+
82
+ // Subscribe to new logs
83
+ const unsubscribe = logger.subscribe((log: LogEvent) => {
84
+ console.log('New log:', log);
85
+ });
86
+ unsubscribe(); // Stop receiving logs
87
+
88
+ // Get current session ID
89
+ const sessionId = logger.getSessionId();
90
+
91
+ // Get current config
92
+ const config = logger.getConfig();
93
+ ```
94
+
95
+ ### Spans (Log Grouping)
96
+
97
+ Group related logs together with timing and status:
98
+
99
+ ```typescript
100
+ // Create a span for an operation
101
+ const span = logger.span('Load user profile');
102
+ span.info('Fetching from API...');
103
+ span.debug('Request payload', { userId: 123 });
104
+
105
+ // End successfully
106
+ span.end(); // status: 'success', duration calculated
107
+
108
+ // Or end with error
109
+ span.fail('Network timeout'); // status: 'error'
110
+ span.fail(new Error('Timeout')); // also logs the error
111
+ ```
112
+
113
+ #### Nested Spans
114
+
115
+ ```typescript
116
+ const requestSpan = logger.span('HTTP Request', { requestId: 'abc-123' });
117
+
118
+ const fetchSpan = requestSpan.span('Fetch Data');
119
+ fetchSpan.info('Fetching...');
120
+ fetchSpan.end();
121
+
122
+ const processSpan = requestSpan.span('Process Data');
123
+ processSpan.info('Processing...');
124
+ processSpan.end();
125
+
126
+ requestSpan.end(); // Parent span ends after children
127
+ ```
128
+
129
+ #### Span Methods
130
+
131
+ ```typescript
132
+ // Get all spans
133
+ const spans = logger.getSpans();
134
+
135
+ // Get specific span
136
+ const span = logger.getSpan(spanId);
137
+
138
+ // Get logs belonging to a span
139
+ const spanLogs = logger.getSpanLogs(spanId);
140
+
141
+ // Subscribe to span events
142
+ const unsub = logger.subscribeSpans((span) => {
143
+ if (span.status === 'error') {
144
+ console.log(`Span ${span.name} failed after ${span.duration}ms`);
145
+ }
146
+ });
147
+ ```
148
+
149
+ ### Context (Tags)
150
+
151
+ Attach contextual information to logs for filtering and correlation:
152
+
153
+ ```typescript
154
+ // Set global context (attached to ALL logs)
155
+ logger.setGlobalContext({ env: 'development', build: '1.2.3' });
156
+
157
+ // Update global context
158
+ logger.updateGlobalContext({ userId: 'user-456' });
159
+
160
+ // Clear global context
161
+ logger.clearGlobalContext();
162
+ ```
163
+
164
+ #### Context-Bound Logger
165
+
166
+ ```typescript
167
+ // Create a logger with specific context
168
+ const reqLogger = logger.withContext({ requestId: 'req-123' });
169
+ reqLogger.info('Request started'); // includes requestId
170
+
171
+ // Chain contexts
172
+ const userLogger = reqLogger.withContext({ userId: 'user-456' });
173
+ userLogger.info('User action'); // includes both requestId and userId
174
+
175
+ // Context loggers can also create spans
176
+ const span = reqLogger.span('Process Request');
177
+ span.info('Processing...'); // inherits requestId
178
+ span.end();
179
+ ```
180
+
181
+ ### Export
182
+
183
+ Export logs for sharing, bug reports, or analysis:
184
+
185
+ ```typescript
186
+ // Export as JSON (pretty printed)
187
+ const json = logger.exportLogs({ format: 'json' });
188
+
189
+ // Export as compact JSON
190
+ const compact = logger.exportLogs({ format: 'json', pretty: false });
191
+
192
+ // Export as human-readable text
193
+ const text = logger.exportLogs({ format: 'text' });
194
+
195
+ // Filter exports
196
+ const filtered = logger.exportLogs({
197
+ format: 'json',
198
+ levels: ['warn', 'error'], // Only warnings and errors
199
+ lastMs: 30000, // Last 30 seconds
200
+ search: 'user', // Contains "user"
201
+ });
202
+
203
+ // Copy to clipboard
204
+ const success = await logger.copyLogs({ format: 'json' });
205
+ if (success) {
206
+ console.log('Logs copied!');
207
+ }
208
+ ```
209
+
210
+ ### Visual Diff
211
+
212
+ Compare objects and log changes with color-coded visualization:
213
+
214
+ ```typescript
215
+ // Log a diff with automatic change detection
216
+ const oldConfig = { theme: 'light', fontSize: 14 };
217
+ const newConfig = { theme: 'dark', fontSize: 14, language: 'en' };
218
+
219
+ const diff = logger.diff('Config updated', oldConfig, newConfig);
220
+ // Logs with visual diff: +1 added, ~1 changed
221
+
222
+ console.log(diff.summary);
223
+ // { added: 1, removed: 0, changed: 1, unchanged: 1 }
224
+
225
+ // Specify log level
226
+ logger.diff('Breaking change', oldApi, newApi, 'warn');
227
+
228
+ // Compute diff without logging
229
+ const result = logger.computeDiff(objA, objB);
230
+ if (result.summary.changed > 0) {
231
+ logger.warn('Objects differ!', result.changes);
232
+ }
233
+ ```
234
+
235
+ #### Diff Utilities
236
+
237
+ ```typescript
238
+ import { computeDiff, createDiffResult, hasChanges, formatValue } from 'devlogger';
239
+
240
+ // Low-level diff computation
241
+ const changes = computeDiff(oldObj, newObj);
242
+ // Returns array of { path, type, oldValue, newValue }
243
+
244
+ // Full diff result with summary
245
+ const result = createDiffResult(oldObj, newObj);
246
+ // { changes: [...], summary: { added, removed, changed, unchanged } }
247
+
248
+ // Quick check for any changes
249
+ if (hasChanges(result)) {
250
+ console.log('Objects are different');
251
+ }
252
+
253
+ // Format values for display
254
+ formatValue({ a: 1 }); // "{a: 1}"
255
+ formatValue([1, 2, 3, 4, 5]); // "[5 items]"
256
+ ```
257
+
258
+ ### Network Capture
259
+
260
+ Automatically track Fetch and XHR requests with spans:
261
+
262
+ ```typescript
263
+ import { NetworkCapture } from 'devlogger';
264
+
265
+ // Install at app start
266
+ NetworkCapture.install();
267
+
268
+ // All fetch calls are now automatically logged
269
+ await fetch('/api/users'); // Creates a span with timing
270
+
271
+ // With configuration
272
+ NetworkCapture.install({
273
+ captureFetch: true, // Hook into fetch (default: true)
274
+ captureXHR: true, // Hook into XHR (default: true)
275
+ includeHeaders: true, // Log request headers (default: false)
276
+ includeBody: true, // Log request body (default: false)
277
+ includeResponse: true, // Log response body (default: false)
278
+ maxResponseLength: 5000, // Max response chars to capture
279
+ ignorePatterns: [ // URLs to ignore
280
+ '/analytics',
281
+ /\.hot-update\./,
282
+ /sockjs/,
283
+ ],
284
+ context: { service: 'api' } // Context for all network logs
285
+ });
286
+
287
+ // Add ignore patterns dynamically
288
+ NetworkCapture.addIgnorePattern('/health');
289
+
290
+ // Check status
291
+ NetworkCapture.isActive();
292
+ NetworkCapture.getConfig();
293
+
294
+ // Uninstall and restore original fetch/XHR
295
+ NetworkCapture.uninstall();
296
+ ```
297
+
298
+ Network requests create spans automatically:
299
+ ```
300
+ [info] GET /api/users
301
+ └─ span: "GET /api/users" (234ms, success)
302
+ ├─ status: 200
303
+ ├─ response: { users: [...] }
304
+ └─ headers: { content-type: "application/json" }
305
+ ```
306
+
307
+ ### Timeline
308
+
309
+ Visualize logs and spans on a canvas-based timeline:
310
+
311
+ ```typescript
312
+ import { createTimeline, Timeline } from 'devlogger';
313
+
314
+ // Create timeline in a container
315
+ const timeline = createTimeline({
316
+ container: '#timeline-container', // CSS selector or HTMLElement
317
+ timeWindow: 60000, // Show last 60 seconds
318
+ refreshInterval: 100, // Refresh rate in ms
319
+ showSpans: true, // Display span bars
320
+ showLogs: true, // Display log markers
321
+ height: 200, // Canvas height in pixels
322
+ });
323
+
324
+ // Update time window
325
+ timeline.setTimeWindow(30000); // Show last 30 seconds
326
+
327
+ // Cleanup when done
328
+ timeline.destroy();
329
+ ```
330
+
331
+ Timeline features:
332
+ - Color-coded log markers (debug/info/warn/error)
333
+ - Span bars with duration and nesting
334
+ - Hover tooltips with details
335
+ - Auto-scroll to follow new logs
336
+ - Time axis with tick marks
337
+
338
+ ### DevLoggerUI
339
+
340
+ The UI overlay provides a visual interface for viewing logs:
341
+
342
+ ```typescript
343
+ // Initialize (creates Shadow DOM host)
344
+ DevLoggerUI.init();
345
+
346
+ // Show/hide panel
347
+ DevLoggerUI.open();
348
+ DevLoggerUI.close();
349
+ DevLoggerUI.toggle();
350
+
351
+ // Open in separate window
352
+ DevLoggerUI.popout();
353
+ DevLoggerUI.closePopout();
354
+ DevLoggerUI.isPopoutOpen();
355
+
356
+ // Filter logs programmatically
357
+ DevLoggerUI.setFilter({
358
+ levels: new Set(['warn', 'error']), // Show only warnings and errors
359
+ search: 'api', // Text search
360
+ file: 'utils', // Filter by file name
361
+ });
362
+ DevLoggerUI.getFilter();
363
+ DevLoggerUI.clearFilter();
364
+
365
+ // Cleanup
366
+ DevLoggerUI.destroy();
367
+
368
+ // State checks
369
+ DevLoggerUI.isVisible();
370
+ DevLoggerUI.isInitialized();
371
+ ```
372
+
373
+ ### Keyboard Shortcut
374
+
375
+ Press `Ctrl+Shift+L` to toggle the debug panel.
376
+
377
+ ### ErrorCapture
378
+
379
+ Automatically capture uncaught errors and unhandled promise rejections:
380
+
381
+ ```typescript
382
+ import { ErrorCapture } from 'devlogger';
383
+
384
+ // Install at app start
385
+ ErrorCapture.install();
386
+
387
+ // With custom configuration
388
+ ErrorCapture.install({
389
+ captureErrors: true, // Capture window.onerror (default: true)
390
+ captureRejections: true, // Capture unhandledrejection (default: true)
391
+ errorPrefix: '[ERROR]', // Prefix for error messages
392
+ rejectionPrefix: '[REJECT]' // Prefix for rejection messages
393
+ });
394
+
395
+ // Check if active
396
+ ErrorCapture.isActive();
397
+
398
+ // Get current config
399
+ ErrorCapture.getConfig();
400
+
401
+ // Uninstall and restore original handlers
402
+ ErrorCapture.uninstall();
403
+ ```
404
+
405
+ All captured errors are automatically logged as `error` level with full stack traces.
406
+
407
+ ### LogPersistence
408
+
409
+ Persist logs to survive page crashes and enable crash recovery:
410
+
411
+ ```typescript
412
+ import { LogPersistence, logger } from 'devlogger';
413
+
414
+ // Enable persistence at app start
415
+ LogPersistence.enable();
416
+
417
+ // Rehydrate logs from previous session
418
+ const count = LogPersistence.rehydrate();
419
+ if (LogPersistence.hadCrash()) {
420
+ logger.warn(`Recovered ${count} logs from previous crash`);
421
+ }
422
+
423
+ // With custom configuration
424
+ LogPersistence.enable({
425
+ storage: 'session', // 'session' (sessionStorage) or 'local' (localStorage)
426
+ maxPersisted: 500, // Max logs to persist
427
+ debounceMs: 100 // Debounce writes for performance
428
+ });
429
+
430
+ // Check if active
431
+ LogPersistence.isActive();
432
+
433
+ // Get persisted logs without importing
434
+ const logs = LogPersistence.getPersistedLogs();
435
+
436
+ // Clear persisted logs
437
+ LogPersistence.clear();
438
+
439
+ // Disable persistence
440
+ LogPersistence.disable();
441
+ ```
442
+
443
+ Logs are persisted automatically after each new log (debounced). On page unload, logs are saved synchronously to ensure no data loss.
444
+
445
+ ## Production Build
446
+
447
+ For production, import from `devlogger/noop` to completely eliminate logging code via tree-shaking:
448
+
449
+ ### Vite
450
+
451
+ ```typescript
452
+ // vite.config.ts
453
+ export default defineConfig({
454
+ resolve: {
455
+ alias: {
456
+ 'devlogger': process.env.NODE_ENV === 'production'
457
+ ? 'devlogger/noop'
458
+ : 'devlogger'
459
+ }
460
+ }
461
+ });
462
+ ```
463
+
464
+ ### Webpack
465
+
466
+ ```javascript
467
+ // webpack.config.js
468
+ module.exports = {
469
+ resolve: {
470
+ alias: {
471
+ 'devlogger': process.env.NODE_ENV === 'production'
472
+ ? 'devlogger/noop'
473
+ : 'devlogger'
474
+ }
475
+ }
476
+ };
477
+ ```
478
+
479
+ ### esbuild
480
+
481
+ ```javascript
482
+ // build.js
483
+ require('esbuild').build({
484
+ alias: {
485
+ 'devlogger': process.env.NODE_ENV === 'production'
486
+ ? 'devlogger/noop'
487
+ : 'devlogger'
488
+ }
489
+ });
490
+ ```
491
+
492
+ The `noop` export provides the same API but all functions are no-ops, resulting in zero runtime overhead after tree-shaking.
493
+
494
+ ## Types
495
+
496
+ ```typescript
497
+ import type {
498
+ LogEvent, LogLevel, LoggerConfig, Source, FilterState,
499
+ ErrorCaptureConfig, LogContext, SpanEvent, SpanStatus, ExportOptions,
500
+ DiffEntry, DiffResult, DiffChangeType, NetworkCaptureConfig, TimelineConfig
501
+ } from 'devlogger';
502
+
503
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
504
+ type SpanStatus = 'running' | 'success' | 'error';
505
+ type LogContext = Record<string, string | number | boolean>;
506
+
507
+ interface Source {
508
+ file: string;
509
+ line: number;
510
+ column?: number;
511
+ function?: string;
512
+ }
513
+
514
+ interface LogEvent {
515
+ id: string;
516
+ timestamp: number;
517
+ level: LogLevel;
518
+ message: string;
519
+ data: unknown[];
520
+ source: Source;
521
+ sessionId: string;
522
+ context?: LogContext; // Attached context/tags
523
+ spanId?: string; // Parent span ID
524
+ }
525
+
526
+ interface SpanEvent {
527
+ id: string;
528
+ name: string;
529
+ startTime: number;
530
+ endTime?: number;
531
+ duration?: number;
532
+ status: SpanStatus;
533
+ parentId?: string; // For nested spans
534
+ context?: LogContext;
535
+ source: Source;
536
+ sessionId: string;
537
+ }
538
+
539
+ interface LoggerConfig {
540
+ maxLogs?: number;
541
+ minLevel?: LogLevel;
542
+ enabled?: boolean;
543
+ }
544
+
545
+ interface ExportOptions {
546
+ format?: 'json' | 'text';
547
+ lastMs?: number; // Filter by time
548
+ levels?: LogLevel[]; // Filter by levels
549
+ search?: string; // Filter by text
550
+ pretty?: boolean; // Pretty print JSON
551
+ }
552
+
553
+ interface FilterState {
554
+ levels: Set<LogLevel>;
555
+ search: string;
556
+ file: string;
557
+ }
558
+
559
+ interface ErrorCaptureConfig {
560
+ captureErrors?: boolean;
561
+ captureRejections?: boolean;
562
+ errorPrefix?: string;
563
+ rejectionPrefix?: string;
564
+ }
565
+
566
+ interface PersistenceConfig {
567
+ storage?: 'session' | 'local';
568
+ maxPersisted?: number;
569
+ debounceMs?: number;
570
+ }
571
+
572
+ // Diff types
573
+ type DiffChangeType = 'added' | 'removed' | 'changed' | 'unchanged';
574
+
575
+ interface DiffEntry {
576
+ path: string; // e.g., "user.profile.name"
577
+ type: DiffChangeType;
578
+ oldValue?: unknown;
579
+ newValue?: unknown;
580
+ }
581
+
582
+ interface DiffResult {
583
+ changes: DiffEntry[];
584
+ summary: {
585
+ added: number;
586
+ removed: number;
587
+ changed: number;
588
+ unchanged: number;
589
+ };
590
+ }
591
+
592
+ interface NetworkCaptureConfig {
593
+ captureFetch?: boolean;
594
+ captureXHR?: boolean;
595
+ includeHeaders?: boolean;
596
+ includeBody?: boolean;
597
+ includeResponse?: boolean;
598
+ maxResponseLength?: number;
599
+ ignorePatterns?: (string | RegExp)[];
600
+ context?: LogContext;
601
+ }
602
+
603
+ interface TimelineConfig {
604
+ container: HTMLElement | string;
605
+ timeWindow?: number;
606
+ refreshInterval?: number;
607
+ showSpans?: boolean;
608
+ showLogs?: boolean;
609
+ height?: number;
610
+ }
611
+ ```
612
+
613
+ ## Architecture
614
+
615
+ ```
616
+ ┌─────────────────────────────────────────────────────────────┐
617
+ │ Your Application │
618
+ │ │
619
+ │ logger.info('message', data) ───────────────────────┐ │
620
+ │ │ │
621
+ └─────────────────────────────────────────────────────────│────┘
622
+
623
+ ┌─────────────────────────────────────────────────────────▼────┐
624
+ │ LoggerCore (Singleton) │
625
+ │ │
626
+ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
627
+ │ │ Capture │──│ Enrich │──│ Store │──│ Notify │ │
628
+ │ │ Source │ │ Metadata │ │ (FIFO) │ │ Subs │ │
629
+ │ └──────────┘ └──────────┘ └──────────┘ └────┬─────┘ │
630
+ │ │ │
631
+ └──────────────────────────────────────────────────│──────────┘
632
+
633
+ ┌────────────────────────────────────────┤
634
+ │ │
635
+ ▼ ▼
636
+ ┌─────────────────────┐ ┌─────────────────────────┐
637
+ │ DevLoggerUI │◄────────────►│ Pop-out Window │
638
+ │ (Shadow DOM) │ Broadcast │ (Separate Window) │
639
+ │ │ Channel │ │
640
+ │ ┌───────────────┐ │ │ ┌───────────────────┐ │
641
+ │ │ Filter Bar │ │ │ │ Synced Logs │ │
642
+ │ │ Log List │ │ │ │ Clear Button │ │
643
+ │ │ Toggle Button │ │ │ │ Connection Status │ │
644
+ │ └───────────────┘ │ │ └───────────────────┘ │
645
+ └─────────────────────┘ └─────────────────────────┘
646
+ ```
647
+
648
+ ## Design Principles
649
+
650
+ 1. **Zero-Throw Policy** - The logger never throws exceptions. If something goes wrong internally, it fails silently to avoid breaking your app.
651
+
652
+ 2. **UI-Agnostic Core** - The `LoggerCore` has no knowledge of the UI. It only manages logs and notifies subscribers.
653
+
654
+ 3. **Shadow DOM Isolation** - The UI uses Shadow DOM to prevent CSS conflicts with your application.
655
+
656
+ 4. **Strict Decoupling** - The logger and UI are completely independent. You can use the logger without the UI, or create your own UI using the `subscribe()` API.
657
+
658
+ 5. **No Side Effects on Import** - Importing the logger doesn't create any DOM elements or start any listeners. You must explicitly call `DevLoggerUI.init()`.
659
+
660
+ ## Browser Support
661
+
662
+ - Chrome/Edge 80+
663
+ - Firefox 78+
664
+ - Safari 14+
665
+
666
+ Requires support for:
667
+ - Shadow DOM
668
+ - BroadcastChannel
669
+ - ES2020+
670
+
671
+ ## License
672
+
673
+ MIT