import type { History, Shortcut } from '../types/generated.js'; export class DOMRenderer { private historyListElement: HTMLElement | null = null; private shortcutsElement: HTMLElement | null = null; constructor() { this.historyListElement = document.getElementById('historyList'); this.shortcutsElement = document.getElementById('shortcuts'); } displayHistory(historyEntries: History[]): void { if (!this.historyListElement) return; if (!historyEntries || historyEntries.length === 0) { this.historyListElement.innerHTML = `

No history yet

Use CLI commands to browse websites

`; return; } this.historyListElement.innerHTML = ''; historyEntries.forEach(item => { const historyItem = this.createHistoryItem(item); this.historyListElement!.appendChild(historyItem); }); } displayShortcuts(shortcutsData: Record): void { if (!this.shortcutsElement) return; if (!shortcutsData || Object.keys(shortcutsData).length === 0) { this.shortcutsElement.innerHTML = `

No shortcuts configured

Add shortcuts to your config file

`; return; } this.shortcutsElement.innerHTML = ''; Object.entries(shortcutsData).forEach(([key, shortcut]) => { const shortcutEl = this.createShortcutElement(key, shortcut); this.shortcutsElement!.appendChild(shortcutEl); }); } showLoading(container: 'history' | 'shortcuts'): void { const element = container === 'history' ? this.historyListElement : this.shortcutsElement; if (element) { element.innerHTML = '
Loading...
'; } } showError(container: 'history' | 'shortcuts', message: string): void { const element = container === 'history' ? this.historyListElement : this.shortcutsElement; if (element) { element.innerHTML = `

Error

${message}

`; } } private createHistoryItem(item: History): HTMLElement { const historyItem = document.createElement('div'); historyItem.className = 'history-item'; const parsed = this.safeParseURL(item.url); const title = item.title && item.title.trim() !== '' ? item.title : (parsed.host || 'Untitled'); const line = document.createElement('div'); line.className = 'history-line'; // Favicon chip (wrapper ensures perfect circle sizing) const iconWrap = document.createElement('div'); iconWrap.className = 'history-favicon-chip'; const icon = document.createElement('img'); icon.className = 'history-favicon-img'; icon.width = 16; icon.height = 16; icon.loading = 'lazy'; icon.referrerPolicy = 'no-referrer'; icon.src = this.buildFaviconURL(item.url); icon.addEventListener('error', () => { iconWrap.style.display = 'none'; }); iconWrap.appendChild(icon); const titleEl = document.createElement('span'); titleEl.className = 'history-title'; titleEl.textContent = title; const sep1 = document.createElement('span'); sep1.className = 'history-sep'; sep1.textContent = ' - '; const domainEl = document.createElement('span'); domainEl.className = 'history-domain'; domainEl.textContent = parsed.host || ''; const sep2 = document.createElement('span'); sep2.className = 'history-sep'; sep2.textContent = ' - '; const urlEl = document.createElement('span'); urlEl.className = 'history-url'; urlEl.textContent = item.url; line.appendChild(iconWrap); line.appendChild(titleEl); line.appendChild(sep1); line.appendChild(domainEl); line.appendChild(sep2); line.appendChild(urlEl); historyItem.appendChild(line); historyItem.addEventListener('click', () => { this.navigateToUrl(item.url); }); return historyItem; } private createShortcutElement(key: string, shortcut: Shortcut): HTMLElement { const shortcutEl = document.createElement('div'); shortcutEl.className = 'shortcut'; shortcutEl.innerHTML = `
${this.escapeHtml(key)}:
${this.escapeHtml(shortcut.description || 'No description')}
`; shortcutEl.addEventListener('click', () => { // Navigate to the base URL of the shortcut (remove %s template parameter) const baseUrl = this.extractBaseUrl(shortcut.url_template); this.navigateToUrl(baseUrl); }); return shortcutEl; } private async navigateToUrl(url: string): Promise { console.log('Navigating to:', url); window.location.href = url; } private extractBaseUrl(templateUrl: string): string { try { // Support both %s and {query} placeholders const candidate = templateUrl.replace(/%s|\{query\}/g, ""); const url = new URL(candidate); // Remove empty query params left after placeholder removal const params = new URLSearchParams(url.search); for (const key of Array.from(params.keys())) { const v = params.get(key); if (v === null || v === "") { params.delete(key); } } url.search = params.toString(); return url.toString(); } catch (error) { console.error('Failed to parse shortcut URL:', templateUrl, error); throw error; } } private escapeHtml(text: string): string { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } private safeParseURL(raw: string): { host: string } { try { const u = new URL(raw); return { host: u.host }; } catch { return { host: '' }; } } private buildFaviconURL(raw: string): string { try { const u = new URL(raw); const scheme = u.protocol && u.protocol !== ':' ? u.protocol : 'https:'; return `${scheme}//${u.host}/favicon.ico`; } catch { return ''; } } }