From a897dbfee4561aed43a762a9735dfdefdd48d8d2 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Mar 2026 02:25:21 +0000 Subject: [PATCH] feat: add LogPanel UI component Co-Authored-By: Claude Opus 4.6 --- client/src/ui/LogPanel.ts | 116 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 client/src/ui/LogPanel.ts diff --git a/client/src/ui/LogPanel.ts b/client/src/ui/LogPanel.ts new file mode 100644 index 0000000..ceb94ac --- /dev/null +++ b/client/src/ui/LogPanel.ts @@ -0,0 +1,116 @@ +import type { LogEntry } from '@dflike/shared'; + +const MAX_ENTRIES = 200; + +const SEVERITY_COLORS: Record = { + error: '#ff6666', + warning: '#f0d060', + info: '#6699cc', +}; + +const SEVERITY_ICONS: Record = { + 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; + } +}