/** * LiveComponent DevTools Overlay * * Entwickler-Werkzeuge fΓΌr LiveComponents mit: * - Component Tree Visualisierung * - Action Log Viewer * - Event Stream Monitor * - Performance Profiling * - State Inspector * - Network Request Timeline * * @module LiveComponentDevTools */ import { Core } from '../core.js'; export class LiveComponentDevTools { constructor() { this.isOpen = false; this.activeTab = 'components'; this.components = new Map(); this.actionLog = []; this.eventLog = []; this.networkLog = []; this.performanceData = []; this.domBadges = new Map(); // Track DOM badges for cleanup this.badgesEnabled = true; // Badge visibility toggle // Performance profiling state this.isRecording = false; this.performanceRecording = []; this.componentRenderTimes = new Map(); this.actionExecutionTimes = new Map(); this.memorySnapshots = []; this.overlay = null; this.isEnabled = this.checkIfEnabled(); if (this.isEnabled) { this.init(); } } /** * Check if DevTools should be enabled */ checkIfEnabled() { // Only in development mode const isDev = document.documentElement.dataset.env === 'development'; // Or explicit activation via localStorage const isExplicitlyEnabled = localStorage.getItem('livecomponent_devtools') === 'true'; return isDev || isExplicitlyEnabled; } /** * Initialize DevTools */ init() { this.createOverlay(); this.attachEventListeners(); this.registerGlobalShortcuts(); this.startMonitoring(); console.log('[LiveComponent DevTools] Initialized'); } /** * Create DevTools overlay DOM structure */ createOverlay() { this.overlay = document.createElement('div'); this.overlay.id = 'livecomponent-devtools'; this.overlay.className = 'lc-devtools'; this.overlay.style.display = 'none'; this.overlay.innerHTML = `
πŸ› οΈ LiveComponent DevTools
`; document.body.appendChild(this.overlay); this.injectStyles(); } /** * Inject DevTools styles */ injectStyles() { const styles = ` .lc-devtools { position: fixed; bottom: 20px; right: 20px; width: 800px; height: 600px; z-index: 999999; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 13px; } .lc-devtools__panel { background: #1e1e1e; border: 1px solid #3c3c3c; border-radius: 8px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); display: flex; flex-direction: column; height: 100%; overflow: hidden; } .lc-devtools__header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: #252526; border-bottom: 1px solid #3c3c3c; color: #cccccc; cursor: move; } .lc-devtools__title { display: flex; align-items: center; gap: 8px; font-weight: 600; } .lc-devtools__icon { font-size: 16px; } .lc-devtools__actions { display: flex; gap: 8px; } .lc-devtools__btn { background: transparent; border: 1px solid #3c3c3c; border-radius: 4px; color: #cccccc; cursor: pointer; padding: 4px 12px; font-size: 13px; transition: all 0.2s; } .lc-devtools__btn:hover { background: #3c3c3c; border-color: #4c4c4c; } .lc-devtools__btn--small { padding: 4px 8px; font-size: 11px; } .lc-devtools__tabs { display: flex; background: #2d2d30; border-bottom: 1px solid #3c3c3c; overflow-x: auto; } .lc-devtools__tab { background: transparent; border: none; border-bottom: 2px solid transparent; color: #969696; cursor: pointer; padding: 10px 16px; font-size: 12px; transition: all 0.2s; white-space: nowrap; } .lc-devtools__tab:hover { color: #cccccc; } .lc-devtools__tab--active { color: #ffffff; border-bottom-color: #007acc; } .lc-devtools__content { flex: 1; overflow: hidden; position: relative; } .lc-devtools__pane { display: none; flex-direction: column; height: 100%; overflow: hidden; } .lc-devtools__pane--active { display: flex; } .lc-devtools__toolbar { display: flex; align-items: center; gap: 8px; padding: 12px 16px; background: #252526; border-bottom: 1px solid #3c3c3c; } .lc-devtools__search { flex: 1; background: #3c3c3c; border: 1px solid #4c4c4c; border-radius: 4px; color: #cccccc; padding: 6px 12px; font-size: 12px; } .lc-devtools__search:focus { outline: none; border-color: #007acc; } .lc-devtools__tree, .lc-devtools__log, .lc-devtools__metrics, .lc-devtools__timeline { flex: 1; overflow-y: auto; padding: 16px; color: #cccccc; } .lc-devtools__tree::-webkit-scrollbar, .lc-devtools__log::-webkit-scrollbar, .lc-devtools__metrics::-webkit-scrollbar, .lc-devtools__timeline::-webkit-scrollbar { width: 8px; } .lc-devtools__tree::-webkit-scrollbar-track, .lc-devtools__log::-webkit-scrollbar-track, .lc-devtools__metrics::-webkit-scrollbar-track, .lc-devtools__timeline::-webkit-scrollbar-track { background: #1e1e1e; } .lc-devtools__tree::-webkit-scrollbar-thumb, .lc-devtools__log::-webkit-scrollbar-thumb, .lc-devtools__metrics::-webkit-scrollbar-thumb, .lc-devtools__timeline::-webkit-scrollbar-thumb { background: #3c3c3c; border-radius: 4px; } .lc-devtools--minimized { height: auto; } .lc-devtools--minimized .lc-devtools__tabs, .lc-devtools--minimized .lc-devtools__content { display: none; } /* DOM Badge Styles */ .lc-dom-badge { position: fixed; background: rgba(30, 30, 30, 0.95); border: 1px solid #007acc; border-radius: 4px; padding: 6px 10px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 11px; color: #cccccc; cursor: pointer; transition: all 0.2s; backdrop-filter: blur(4px); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); pointer-events: auto; } .lc-dom-badge:hover { background: rgba(30, 30, 30, 1); border-color: #4ec9b0; transform: scale(1.05); } .lc-dom-badge.lc-badge-active { animation: badge-pulse 0.5s ease-out; border-color: #4ec9b0; } @keyframes badge-pulse { 0% { transform: scale(1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } 50% { transform: scale(1.1); box-shadow: 0 4px 16px rgba(78, 201, 176, 0.5); } 100% { transform: scale(1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } } .lc-badge-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; } .lc-badge-icon { font-size: 12px; } .lc-badge-name { color: #569cd6; font-weight: 600; } .lc-badge-id { color: #858585; font-size: 10px; } .lc-badge-activity { display: flex; gap: 8px; font-size: 10px; color: #4ec9b0; } .lc-badge-actions { color: #dcdcaa; } `; const styleSheet = document.createElement('style'); styleSheet.textContent = styles; document.head.appendChild(styleSheet); } /** * Attach event listeners */ attachEventListeners() { // Header actions this.overlay.querySelector('[data-action="toggle-badges"]')?.addEventListener('click', () => { this.toggleBadges(); }); this.overlay.querySelector('[data-action="minimize"]')?.addEventListener('click', () => { this.toggleMinimize(); }); this.overlay.querySelector('[data-action="close"]')?.addEventListener('click', () => { this.close(); }); // Tab switching this.overlay.querySelectorAll('[data-tab]').forEach(tab => { tab.addEventListener('click', (e) => { this.switchTab(e.target.dataset.tab); }); }); // Toolbar actions this.overlay.querySelector('[data-action="refresh-components"]')?.addEventListener('click', () => { this.refreshComponents(); }); this.overlay.querySelector('[data-action="clear-actions"]')?.addEventListener('click', () => { this.clearActionLog(); }); this.overlay.querySelector('[data-action="clear-events"]')?.addEventListener('click', () => { this.clearEventLog(); }); this.overlay.querySelector('[data-action="record-performance"]')?.addEventListener('click', () => { this.togglePerformanceRecording(); }); this.overlay.querySelector('[data-action="clear-performance"]')?.addEventListener('click', () => { this.clearPerformanceData(); }); this.overlay.querySelector('[data-action="clear-network"]')?.addEventListener('click', () => { this.clearNetworkLog(); }); // Make header draggable this.makeDraggable(); } /** * Register global keyboard shortcuts */ registerGlobalShortcuts() { document.addEventListener('keydown', (e) => { // Ctrl/Cmd + Shift + D - Toggle DevTools if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'D') { e.preventDefault(); this.toggle(); } }); } /** * Start monitoring LiveComponents */ startMonitoring() { // Intercept Core.emit for event logging this.interceptCoreEmit(); // Monitor component initialization this.monitorComponentInit(); // Monitor network requests this.monitorNetworkRequests(); // Connect to LiveComponent manager this.connectToLiveComponent(); // Initialize DOM badges this.initializeDomBadges(); } /** * Connect to LiveComponent manager for action/event logging */ connectToLiveComponent() { // Wait for LiveComponent to be available const checkLiveComponent = () => { if (window.LiveComponent) { window.LiveComponent.enableDevTools(this); console.log('[LiveComponent DevTools] Connected to LiveComponent manager'); } else { // Retry after a short delay setTimeout(checkLiveComponent, 100); } }; checkLiveComponent(); } /** * Toggle DevTools visibility */ toggle() { if (this.isOpen) { this.close(); } else { this.open(); } } /** * Open DevTools */ open() { this.overlay.style.display = 'block'; this.isOpen = true; this.refreshComponents(); // Show badges when DevTools opens if (this.badgesEnabled) { this.updateDomBadges(); } Core.emit('devtools:opened'); } /** * Close DevTools */ close() { this.overlay.style.display = 'none'; this.isOpen = false; Core.emit('devtools:closed'); } /** * Toggle minimize state */ toggleMinimize() { this.overlay.classList.toggle('lc-devtools--minimized'); } /** * Switch active tab */ switchTab(tabName) { this.activeTab = tabName; // Update tab buttons this.overlay.querySelectorAll('[data-tab]').forEach(tab => { tab.classList.toggle('lc-devtools__tab--active', tab.dataset.tab === tabName); }); // Update panes this.overlay.querySelectorAll('[data-pane]').forEach(pane => { pane.classList.toggle('lc-devtools__pane--active', pane.dataset.pane === tabName); }); // Refresh content for active tab this.refreshActiveTab(); } /** * Make overlay draggable */ makeDraggable() { const header = this.overlay.querySelector('.lc-devtools__header'); let isDragging = false; let currentX; let currentY; let initialX; let initialY; header.addEventListener('mousedown', (e) => { if (e.target.closest('.lc-devtools__actions')) return; isDragging = true; initialX = e.clientX - this.overlay.offsetLeft; initialY = e.clientY - this.overlay.offsetTop; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; e.preventDefault(); currentX = e.clientX - initialX; currentY = e.clientY - initialY; this.overlay.style.left = currentX + 'px'; this.overlay.style.top = currentY + 'px'; this.overlay.style.right = 'auto'; this.overlay.style.bottom = 'auto'; }); document.addEventListener('mouseup', () => { isDragging = false; }); } /** * Refresh active tab content */ refreshActiveTab() { switch (this.activeTab) { case 'components': this.renderComponents(); break; case 'actions': this.renderActionLog(); break; case 'events': this.renderEventLog(); break; case 'performance': this.renderPerformanceData(); break; case 'network': this.renderNetworkLog(); break; } } /** * Refresh components tree */ refreshComponents() { // Find all LiveComponent elements in DOM this.components.clear(); document.querySelectorAll('[data-component-id]').forEach(element => { const componentId = element.dataset.componentId; const componentName = element.dataset.componentName || 'Unknown'; this.components.set(componentId, { id: componentId, name: componentName, element: element, state: this.extractComponentState(element), props: this.extractComponentProps(element) }); }); this.renderComponents(); } /** * Extract component state from element */ extractComponentState(element) { // Try to get state from data attribute or custom property return element.__liveComponentState || {}; } /** * Extract component props from element */ extractComponentProps(element) { const props = {}; // Extract data-* attributes as props for (const [key, value] of Object.entries(element.dataset)) { if (key !== 'componentId' && key !== 'componentName') { props[key] = value; } } return props; } /** * Render components tree */ renderComponents() { const container = this.overlay.querySelector('[data-content="components"]'); if (!container) return; if (this.components.size === 0) { container.innerHTML = '
No LiveComponents found
'; return; } let html = '
'; for (const [id, component] of this.components) { html += `
${component.name} #${id}
`; } html += '
'; container.innerHTML = html; // Add click handlers for component items container.querySelectorAll('.lc-component-item').forEach(item => { item.addEventListener('click', () => { const details = item.querySelector('.lc-component-details'); details.style.display = details.style.display === 'none' ? 'block' : 'none'; }); }); } /** * Log action execution * Called by LiveComponent manager */ logAction(componentId, actionName, params, startTime, endTime, success, error = null) { const duration = endTime - startTime; this.actionLog.unshift({ timestamp: Date.now(), componentId, actionName, params, duration, success, error }); // Keep only last 100 actions if (this.actionLog.length > 100) { this.actionLog.pop(); } // Track performance data if recording if (this.isRecording) { this.recordActionExecution(componentId, actionName, duration, startTime, endTime); } // Update DOM badge activity this.updateBadgeActivity(componentId); if (this.activeTab === 'actions') { this.renderActionLog(); } } /** * Render action log */ renderActionLog() { const container = this.overlay.querySelector('[data-content="actions"]'); if (!container) return; if (this.actionLog.length === 0) { container.innerHTML = '
No actions logged
'; return; } let html = '
'; for (const action of this.actionLog) { const time = new Date(action.timestamp).toLocaleTimeString(); const statusIcon = action.success ? 'βœ“' : 'βœ—'; const statusColor = action.success ? '#4ec9b0' : '#f48771'; const paramsPreview = JSON.stringify(action.params).substring(0, 50); html += `
${statusIcon} ${time} ${action.componentId} ${action.actionName}() ${action.duration.toFixed(2)}ms
${action.error ? `
Error: ${action.error}
` : ''} ${Object.keys(action.params).length > 0 ? `
${paramsPreview}${JSON.stringify(action.params).length > 50 ? '...' : ''}
` : ''}
`; } html += '
'; container.innerHTML = html; // Add click handlers to show full details container.querySelectorAll('.lc-log-entry').forEach(entry => { entry.addEventListener('click', () => { const index = parseInt(entry.dataset.actionIndex); const action = this.actionLog[index]; console.log('[LiveComponent DevTools] Action details:', action); }); }); } /** * Clear action log */ clearActionLog() { this.actionLog = []; this.renderActionLog(); } /** * Log event * Called by LiveComponent manager or Core.emit interception */ logEvent(eventName, data, source = 'client') { this.eventLog.unshift({ timestamp: Date.now(), eventName, data, source }); // Keep only last 100 events if (this.eventLog.length > 100) { this.eventLog.pop(); } if (this.activeTab === 'events') { this.renderEventLog(); } } /** * Render event log */ renderEventLog() { const container = this.overlay.querySelector('[data-content="events"]'); if (!container) return; if (this.eventLog.length === 0) { container.innerHTML = '
No events logged
'; return; } let html = '
'; for (const event of this.eventLog) { const time = new Date(event.timestamp).toLocaleTimeString(); const sourceColor = event.source === 'server' ? '#ce9178' : '#4ec9b0'; const sourceIcon = event.source === 'server' ? 'β¬…' : '⬆'; html += `
${sourceIcon} ${time} ${event.eventName} [${event.source}]
${event.data && Object.keys(event.data).length > 0 ? `
${JSON.stringify(event.data).substring(0, 60)}${JSON.stringify(event.data).length > 60 ? '...' : ''}
` : ''}
`; } html += '
'; container.innerHTML = html; // Add click handlers to show full details container.querySelectorAll('.lc-log-entry').forEach(entry => { entry.addEventListener('click', () => { const index = parseInt(entry.dataset.eventIndex); const event = this.eventLog[index]; console.log('[LiveComponent DevTools] Event details:', event); }); }); } /** * Clear event log */ clearEventLog() { this.eventLog = []; this.renderEventLog(); } /** * Clear performance data */ clearPerformanceData() { this.performanceData = []; this.performanceRecording = []; this.componentRenderTimes.clear(); this.actionExecutionTimes.clear(); this.memorySnapshots = []; this.renderPerformanceData(); } /** * Toggle performance recording */ togglePerformanceRecording() { this.isRecording = !this.isRecording; const recordBtn = this.overlay?.querySelector('[data-action="record-performance"]'); if (recordBtn) { if (this.isRecording) { recordBtn.textContent = 'β–  Stop'; recordBtn.style.color = '#f48771'; this.startPerformanceRecording(); } else { recordBtn.textContent = '● Record'; recordBtn.style.color = '#cccccc'; this.stopPerformanceRecording(); } } } /** * Start performance recording */ startPerformanceRecording() { console.log('[LiveComponent DevTools] Performance recording started'); this.performanceRecording = []; this.memorySnapshots = []; // Take initial memory snapshot if available if (performance.memory) { this.takeMemorySnapshot(); } // Start periodic memory snapshots (every 500ms) this.memorySnapshotInterval = setInterval(() => { if (this.isRecording && performance.memory) { this.takeMemorySnapshot(); } }, 500); } /** * Stop performance recording */ stopPerformanceRecording() { console.log('[LiveComponent DevTools] Performance recording stopped'); if (this.memorySnapshotInterval) { clearInterval(this.memorySnapshotInterval); } // Take final memory snapshot if (performance.memory) { this.takeMemorySnapshot(); } this.renderPerformanceData(); } /** * Record action execution for performance profiling */ recordActionExecution(componentId, actionName, duration, startTime, endTime) { this.performanceRecording.push({ type: 'action', componentId, actionName, duration, startTime, endTime, timestamp: Date.now() }); // Update action execution times map const key = `${componentId}:${actionName}`; if (!this.actionExecutionTimes.has(key)) { this.actionExecutionTimes.set(key, []); } this.actionExecutionTimes.get(key).push(duration); // Keep only last 100 recordings if (this.performanceRecording.length > 100) { this.performanceRecording.shift(); } } /** * Record component render for performance profiling */ recordComponentRender(componentId, duration, startTime, endTime) { if (!this.isRecording) return; this.performanceRecording.push({ type: 'render', componentId, duration, startTime, endTime, timestamp: Date.now() }); // Update component render times map if (!this.componentRenderTimes.has(componentId)) { this.componentRenderTimes.set(componentId, []); } this.componentRenderTimes.get(componentId).push(duration); // Keep only last 100 recordings if (this.performanceRecording.length > 100) { this.performanceRecording.shift(); } } /** * Take memory snapshot */ takeMemorySnapshot() { if (!performance.memory) return; this.memorySnapshots.push({ timestamp: Date.now(), usedJSHeapSize: performance.memory.usedJSHeapSize, totalJSHeapSize: performance.memory.totalJSHeapSize, jsHeapSizeLimit: performance.memory.jsHeapSizeLimit }); // Keep only last 100 snapshots if (this.memorySnapshots.length > 100) { this.memorySnapshots.shift(); } } /** * Render performance data */ renderPerformanceData() { const container = this.overlay.querySelector('[data-content="performance"]'); if (!container) return; if (this.performanceRecording.length === 0 && this.memorySnapshots.length === 0) { container.innerHTML = `

No performance data recorded

Click "● Record" to start profiling

`; return; } let html = '
'; // Performance Summary html += this.renderPerformanceSummary(); // Flamegraph html += this.renderFlamegraph(); // Timeline html += this.renderPerformanceTimeline(); // Memory Usage Chart if (this.memorySnapshots.length > 0) { html += this.renderMemoryChart(); } html += '
'; container.innerHTML = html; } /** * Render performance summary */ renderPerformanceSummary() { const actionCount = this.performanceRecording.filter(r => r.type === 'action').length; const renderCount = this.performanceRecording.filter(r => r.type === 'render').length; const avgActionTime = actionCount > 0 ? this.performanceRecording .filter(r => r.type === 'action') .reduce((sum, r) => sum + r.duration, 0) / actionCount : 0; const avgRenderTime = renderCount > 0 ? this.performanceRecording .filter(r => r.type === 'render') .reduce((sum, r) => sum + r.duration, 0) / renderCount : 0; const totalDuration = this.performanceRecording.reduce((sum, r) => sum + r.duration, 0); return `

Performance Summary

Total Events
${this.performanceRecording.length}
Actions
${actionCount}
Renders
${renderCount}
Avg Action
${avgActionTime.toFixed(2)}ms
Total Duration
${totalDuration.toFixed(2)}ms
`; } /** * Render flamegraph */ renderFlamegraph() { if (this.actionExecutionTimes.size === 0 && this.componentRenderTimes.size === 0) { return ''; } let html = `

Execution Breakdown (Flamegraph)

`; // Calculate total time for each component/action const breakdown = []; // Process action execution times for (const [key, times] of this.actionExecutionTimes) { const [componentId, actionName] = key.split(':'); const totalTime = times.reduce((sum, t) => sum + t, 0); const avgTime = totalTime / times.length; const count = times.length; breakdown.push({ type: 'action', componentId, label: actionName, totalTime, avgTime, count, color: '#dcdcaa' }); } // Process component render times for (const [componentId, times] of this.componentRenderTimes) { const totalTime = times.reduce((sum, t) => sum + t, 0); const avgTime = totalTime / times.length; const count = times.length; breakdown.push({ type: 'render', componentId, label: 'render', totalTime, avgTime, count, color: '#569cd6' }); } // Sort by total time descending breakdown.sort((a, b) => b.totalTime - a.totalTime); // Calculate max time for bar width const maxTime = breakdown[0]?.totalTime || 1; // Render bars html += '
'; for (const item of breakdown.slice(0, 10)) { // Top 10 const widthPercent = (item.totalTime / maxTime) * 100; const componentName = this.components.get(item.componentId)?.name || item.componentId.substring(0, 8); html += `
${componentName} .${item.label}() Γ—${item.count} ${item.totalTime.toFixed(2)}ms (avg: ${item.avgTime.toFixed(2)}ms)
`; } html += '
'; html += '
'; return html; } /** * Render performance timeline */ renderPerformanceTimeline() { if (this.performanceRecording.length === 0) { return ''; } let html = `

Execution Timeline

`; // Get time range const minTime = Math.min(...this.performanceRecording.map(r => r.startTime)); const maxTime = Math.max(...this.performanceRecording.map(r => r.endTime)); const timeRange = maxTime - minTime; // Render timeline bars html += '
'; let yOffset = 0; const barHeight = 16; const maxBars = Math.floor(200 / barHeight); for (const record of this.performanceRecording.slice(0, maxBars)) { const leftPercent = ((record.startTime - minTime) / timeRange) * 100; const widthPercent = (record.duration / timeRange) * 100; const color = record.type === 'action' ? '#dcdcaa' : '#569cd6'; const componentName = this.components.get(record.componentId)?.name || record.componentId.substring(0, 8); const label = record.type === 'action' ? record.actionName : 'render'; html += `
${componentName}.${label}()
`; yOffset += barHeight; } html += '
'; // Time labels html += `
0ms ${(timeRange / 2).toFixed(2)}ms ${timeRange.toFixed(2)}ms
`; html += '
'; return html; } /** * Render memory usage chart */ renderMemoryChart() { if (this.memorySnapshots.length === 0) { return ''; } let html = `

Memory Usage

`; // Get current memory stats const current = this.memorySnapshots[this.memorySnapshots.length - 1]; const initial = this.memorySnapshots[0]; const delta = current.usedJSHeapSize - initial.usedJSHeapSize; const deltaPercent = (delta / initial.usedJSHeapSize) * 100; html += `
Used Heap
${this.formatBytes(current.usedJSHeapSize)}
Total Heap
${this.formatBytes(current.totalJSHeapSize)}
Heap Limit
${this.formatBytes(current.jsHeapSizeLimit)}
Delta
${delta > 0 ? '+' : ''}${this.formatBytes(delta)}
`; // Render memory chart const maxMemory = Math.max(...this.memorySnapshots.map(s => s.usedJSHeapSize)); const chartHeight = 100; html += '
'; // Draw memory line chart let points = ''; const step = 100 / (this.memorySnapshots.length - 1); for (let i = 0; i < this.memorySnapshots.length; i++) { const snapshot = this.memorySnapshots[i]; const x = i * step; const y = chartHeight - (snapshot.usedJSHeapSize / maxMemory) * chartHeight; points += `${x},${y} `; } html += ` `; html += '
'; html += '
'; return html; } /** * Format bytes to human-readable string */ formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } /** * Clear network log */ clearNetworkLog() { this.networkLog = []; this.renderNetworkLog(); } /** * Render network log */ renderNetworkLog() { const container = this.overlay.querySelector('[data-content="network"]'); if (!container) return; if (this.networkLog.length === 0) { container.innerHTML = '
No requests logged
'; return; } let html = '
'; for (const request of this.networkLog) { const time = new Date(request.timestamp).toLocaleTimeString(); html += `
${time} ${request.method} ${request.url} ${request.status} ${request.duration.toFixed(2)}ms
`; } html += '
'; container.innerHTML = html; } /** * Initialize DOM badges for all components */ initializeDomBadges() { // Observe DOM for new components const observer = new MutationObserver(() => { this.updateDomBadges(); }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-component-id'] }); // Initial badge creation this.updateDomBadges(); } /** * Update DOM badges for all LiveComponents */ updateDomBadges() { if (!this.badgesEnabled) return; // Find all LiveComponent elements document.querySelectorAll('[data-component-id]').forEach(element => { const componentId = element.dataset.componentId; // Skip if badge already exists if (this.domBadges.has(componentId)) { const existingBadge = this.domBadges.get(componentId); // Update badge position if element moved this.updateBadgePosition(existingBadge, element); return; } // Create new badge this.createDomBadge(componentId, element); }); // Clean up badges for removed components this.cleanupRemovedBadges(); } /** * Create DOM badge for a component */ createDomBadge(componentId, element) { const badge = document.createElement('div'); badge.className = 'lc-dom-badge'; badge.dataset.componentId = componentId; const componentName = element.dataset.componentName || 'Unknown'; badge.innerHTML = `
⚑ ${componentName} #${componentId.substring(0, 8)}
0 actions
`; // Position badge relative to element this.positionBadge(badge, element); // Add interaction handlers badge.addEventListener('click', (e) => { e.stopPropagation(); this.highlightComponent(componentId); this.focusComponentInDevTools(componentId); }); badge.addEventListener('mouseenter', () => { this.highlightElement(element); }); badge.addEventListener('mouseleave', () => { this.unhighlightElement(element); }); document.body.appendChild(badge); this.domBadges.set(componentId, { badge, element, actionCount: 0 }); } /** * Position badge relative to component element */ positionBadge(badge, element) { const rect = element.getBoundingClientRect(); badge.style.position = 'fixed'; badge.style.top = `${rect.top + window.scrollY}px`; badge.style.left = `${rect.left + window.scrollX}px`; badge.style.zIndex = '999998'; } /** * Update badge position when element moves */ updateBadgePosition(badgeData, element) { const rect = element.getBoundingClientRect(); badgeData.badge.style.top = `${rect.top + window.scrollY}px`; badgeData.badge.style.left = `${rect.left + window.scrollX}px`; } /** * Update badge activity counter */ updateBadgeActivity(componentId) { const badgeData = this.domBadges.get(componentId); if (!badgeData) return; badgeData.actionCount++; const actionsSpan = badgeData.badge.querySelector('.lc-badge-actions'); if (actionsSpan) { actionsSpan.textContent = `${badgeData.actionCount} action${badgeData.actionCount !== 1 ? 's' : ''}`; } // Flash badge to show activity badgeData.badge.classList.add('lc-badge-active'); setTimeout(() => { badgeData.badge.classList.remove('lc-badge-active'); }, 500); } /** * Highlight component element */ highlightElement(element) { element.style.outline = '2px solid #007acc'; element.style.outlineOffset = '2px'; } /** * Remove element highlight */ unhighlightElement(element) { element.style.outline = ''; element.style.outlineOffset = ''; } /** * Highlight component in DevTools */ highlightComponent(componentId) { if (!this.isOpen) { this.open(); } this.switchTab('components'); // Highlight component in tree const componentItem = this.overlay.querySelector(`[data-component-id="${componentId}"]`); if (componentItem) { componentItem.scrollIntoView({ behavior: 'smooth', block: 'center' }); componentItem.style.background = 'rgba(0, 122, 204, 0.2)'; setTimeout(() => { componentItem.style.background = ''; }, 2000); } } /** * Focus component in DevTools tree */ focusComponentInDevTools(componentId) { const componentItem = this.overlay.querySelector(`[data-component-id="${componentId}"]`); if (componentItem) { // Expand component details const details = componentItem.querySelector('.lc-component-details'); if (details) { details.style.display = 'block'; } } } /** * Clean up badges for removed components */ cleanupRemovedBadges() { const existingComponents = new Set( Array.from(document.querySelectorAll('[data-component-id]')) .map(el => el.dataset.componentId) ); for (const [componentId, badgeData] of this.domBadges) { if (!existingComponents.has(componentId)) { badgeData.badge.remove(); this.domBadges.delete(componentId); } } } /** * Toggle DOM badges visibility */ toggleBadges() { this.badgesEnabled = !this.badgesEnabled; if (this.badgesEnabled) { // Show all badges for (const badgeData of this.domBadges.values()) { badgeData.badge.style.display = 'block'; } this.updateDomBadges(); } else { // Hide all badges for (const badgeData of this.domBadges.values()) { badgeData.badge.style.display = 'none'; } } // Update button state const badgeBtn = this.overlay?.querySelector('[data-action="toggle-badges"]'); if (badgeBtn) { badgeBtn.style.opacity = this.badgesEnabled ? '1' : '0.5'; } } /** * Remove all DOM badges */ removeAllBadges() { for (const badgeData of this.domBadges.values()) { badgeData.badge.remove(); } this.domBadges.clear(); } /** * Intercept Core.emit for event logging */ interceptCoreEmit() { const originalEmit = Core.emit; Core.emit = (eventName, data) => { // Log as client-side event (from Core.emit) this.logEvent(eventName, data, 'client'); return originalEmit.call(Core, eventName, data); }; } /** * Monitor component initialization */ monitorComponentInit() { Core.on('component:initialized', (data) => { this.refreshComponents(); }); Core.on('component:destroyed', (data) => { this.refreshComponents(); }); } /** * Monitor network requests */ monitorNetworkRequests() { // Intercept fetch const originalFetch = window.fetch; window.fetch = async (...args) => { const startTime = performance.now(); const url = typeof args[0] === 'string' ? args[0] : args[0].url; const method = args[1]?.method || 'GET'; try { const response = await originalFetch(...args); const duration = performance.now() - startTime; this.networkLog.unshift({ timestamp: Date.now(), method, url, status: response.status, duration }); // Keep only last 50 requests if (this.networkLog.length > 50) { this.networkLog.pop(); } if (this.activeTab === 'network') { this.renderNetworkLog(); } return response; } catch (error) { const duration = performance.now() - startTime; this.networkLog.unshift({ timestamp: Date.now(), method, url, status: 'Error', duration }); throw error; } }; } } // Auto-initialize if enabled if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { window.__liveComponentDevTools = new LiveComponentDevTools(); }); } else { window.__liveComponentDevTools = new LiveComponentDevTools(); }