/**
* 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 = `
`;
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 += `
Props:
${JSON.stringify(component.props, null, 2)}
State:
${JSON.stringify(component.state, null, 2)}
`;
}
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}
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 += `
`;
}
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 = `
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();
}