feat: add LogPanel UI component
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import type { LogEntry } from '@dflike/shared';
|
||||
|
||||
const MAX_ENTRIES = 200;
|
||||
|
||||
const SEVERITY_COLORS: Record<string, string> = {
|
||||
error: '#ff6666',
|
||||
warning: '#f0d060',
|
||||
info: '#6699cc',
|
||||
};
|
||||
|
||||
const SEVERITY_ICONS: Record<string, string> = {
|
||||
error: '\u25cf',
|
||||
warning: '\u25b2',
|
||||
info: '\u25cb',
|
||||
};
|
||||
|
||||
export class LogPanel {
|
||||
private container: HTMLDivElement;
|
||||
private logEl: HTMLDivElement;
|
||||
private emptyEl: HTMLDivElement;
|
||||
|
||||
constructor() {
|
||||
this.container = document.createElement('div');
|
||||
this.container.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: 4px;
|
||||
`;
|
||||
|
||||
this.emptyEl = document.createElement('div');
|
||||
this.emptyEl.style.cssText = `
|
||||
color: #8878a8;
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
`;
|
||||
this.emptyEl.textContent = 'No log entries yet...';
|
||||
this.container.appendChild(this.emptyEl);
|
||||
|
||||
this.logEl = document.createElement('div');
|
||||
this.logEl.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
`;
|
||||
this.container.appendChild(this.logEl);
|
||||
}
|
||||
|
||||
getElement(): HTMLDivElement {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
addEntry(entry: LogEntry): void {
|
||||
this.emptyEl.style.display = 'none';
|
||||
const el = this.createLogLine(entry);
|
||||
this.logEl.insertBefore(el, this.logEl.firstChild);
|
||||
while (this.logEl.children.length > MAX_ENTRIES) {
|
||||
this.logEl.removeChild(this.logEl.lastChild!);
|
||||
}
|
||||
}
|
||||
|
||||
loadHistory(entries: LogEntry[]): void {
|
||||
this.logEl.innerHTML = '';
|
||||
if (entries.length === 0) {
|
||||
this.emptyEl.style.display = '';
|
||||
return;
|
||||
}
|
||||
this.emptyEl.style.display = 'none';
|
||||
for (let i = entries.length - 1; i >= 0; i--) {
|
||||
this.logEl.appendChild(this.createLogLine(entries[i]));
|
||||
}
|
||||
}
|
||||
|
||||
private createLogLine(entry: LogEntry): HTMLDivElement {
|
||||
const el = document.createElement('div');
|
||||
el.style.cssText = `
|
||||
font-size: 9px;
|
||||
padding: 2px 0;
|
||||
border-bottom: 1px solid #1c1450;
|
||||
line-height: 1.5;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: baseline;
|
||||
`;
|
||||
|
||||
const timeStr = new Date(entry.timestamp).toLocaleTimeString('en-US', { hour12: false });
|
||||
const color = SEVERITY_COLORS[entry.severity] ?? '#a0a0c0';
|
||||
const icon = SEVERITY_ICONS[entry.severity] ?? '';
|
||||
|
||||
const timeEl = document.createElement('span');
|
||||
timeEl.style.cssText = 'color: #666; flex-shrink: 0;';
|
||||
timeEl.textContent = timeStr;
|
||||
|
||||
const iconEl = document.createElement('span');
|
||||
iconEl.style.cssText = `color: ${color}; flex-shrink: 0;`;
|
||||
iconEl.textContent = icon;
|
||||
|
||||
const catEl = document.createElement('span');
|
||||
catEl.style.cssText = 'color: #8878a8; flex-shrink: 0;';
|
||||
catEl.textContent = `[${entry.category}]`;
|
||||
|
||||
const msgEl = document.createElement('span');
|
||||
msgEl.style.cssText = `color: ${color};`;
|
||||
msgEl.textContent = entry.message;
|
||||
|
||||
el.appendChild(timeEl);
|
||||
el.appendChild(iconEl);
|
||||
el.appendChild(catEl);
|
||||
el.appendChild(msgEl);
|
||||
|
||||
return el;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user