From 4fe41b7cf03f4fd34f06734799654410e1fc2c86 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 8 Mar 2026 04:47:22 +0000 Subject: [PATCH] feat: add SuperlativesPanel UI component Co-Authored-By: Claude Opus 4.6 --- client/src/ui/SuperlativesPanel.ts | 237 +++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 client/src/ui/SuperlativesPanel.ts diff --git a/client/src/ui/SuperlativesPanel.ts b/client/src/ui/SuperlativesPanel.ts new file mode 100644 index 0000000..444a170 --- /dev/null +++ b/client/src/ui/SuperlativesPanel.ts @@ -0,0 +1,237 @@ +import type { SuperlativesData } from '@dflike/shared'; + +type SuperlativeCategory = { + key: keyof SuperlativesData; + label: string; +}; + +const CATEGORIES: SuperlativeCategory[] = [ + { key: 'mostLoved', label: 'Most Loved' }, + { key: 'mostReviled', label: 'Most Reviled' }, + { key: 'mostPopular', label: 'Most Popular' }, + { key: 'mostAnnoying', label: 'Most Annoying' }, + { key: 'mostOutgoing', label: 'Most Outgoing' }, + { key: 'shyest', label: 'Shyest' }, + { key: 'socialButterfly', label: 'Social Butterfly' }, + { key: 'loneliest', label: 'Loneliest' }, + { key: 'mostDevoted', label: 'Most Devoted' }, + { key: 'mostPolarizing', label: 'Most Polarizing' }, + { key: 'biggestHeartbreaker', label: 'Heartbreaker' }, +]; + +export class SuperlativesPanel { + private container: HTMLDivElement; + private tab: HTMLDivElement; + private panel: HTMLDivElement; + private contentEl: HTMLDivElement; + private expanded = false; + private categoryElements: Map = new Map(); + + constructor() { + // Outer container for positioning + this.container = document.createElement('div'); + this.container.style.cssText = ` + position: fixed; + left: 0; + top: 50%; + transform: translateY(-50%); + z-index: 1000; + display: flex; + flex-direction: row; + font-family: 'Press Start 2P', monospace; + transition: transform 0.35s ease; + `; + + // Main panel + this.panel = document.createElement('div'); + this.panel.style.cssText = ` + width: 240px; + background: #1a1a2e; + border: 3px solid #e0d0b0; + border-left: none; + padding: 0; + box-sizing: border-box; + max-height: 80vh; + overflow-y: auto; + `; + + // Inner frame (EarthBound double-border) + const innerFrame = document.createElement('div'); + innerFrame.style.cssText = ` + border: 2px solid #8878a8; + margin: 4px; + padding: 8px; + `; + + // Header + const header = document.createElement('div'); + header.style.cssText = ` + text-align: center; + color: #e0d0b0; + font-size: 8px; + padding: 4px 0 8px 0; + border-bottom: 1px solid #8878a8; + margin-bottom: 8px; + `; + header.textContent = 'SUPERLATIVES'; + + // Content area + this.contentEl = document.createElement('div'); + + for (const cat of CATEGORIES) { + const row = document.createElement('div'); + row.style.cssText = 'margin-bottom: 10px;'; + + const label = document.createElement('div'); + label.style.cssText = ` + color: #8878a8; + font-size: 6px; + margin-bottom: 2px; + text-transform: uppercase; + `; + label.textContent = cat.label; + + const entryRow = document.createElement('div'); + entryRow.style.cssText = ` + display: flex; + justify-content: space-between; + align-items: baseline; + `; + + const nameEl = document.createElement('span'); + nameEl.style.cssText = ` + color: #58d858; + font-size: 7px; + cursor: pointer; + text-decoration: none; + `; + nameEl.textContent = '\u2014'; + nameEl.addEventListener('mouseenter', () => { + if (nameEl.dataset.entityId) { + nameEl.style.color = '#88ff88'; + nameEl.style.textDecoration = 'underline'; + } + }); + nameEl.addEventListener('mouseleave', () => { + nameEl.style.color = '#58d858'; + nameEl.style.textDecoration = 'none'; + }); + nameEl.addEventListener('click', () => { + const entityId = nameEl.dataset.entityId; + if (entityId) { + document.dispatchEvent(new CustomEvent('superlative-follow', { + detail: { entityId: parseInt(entityId, 10) }, + })); + } + }); + + const valueEl = document.createElement('span'); + valueEl.style.cssText = ` + color: #a0a0c0; + font-size: 6px; + margin-left: 4px; + flex-shrink: 0; + `; + + entryRow.appendChild(nameEl); + entryRow.appendChild(valueEl); + row.appendChild(label); + row.appendChild(entryRow); + this.contentEl.appendChild(row); + + this.categoryElements.set(cat.key, { nameEl, valueEl }); + } + + innerFrame.appendChild(header); + innerFrame.appendChild(this.contentEl); + this.panel.appendChild(innerFrame); + + // Tab + this.tab = document.createElement('div'); + this.tab.style.cssText = ` + width: 24px; + height: 48px; + background: #1a1a2e; + border: 3px solid #e0d0b0; + border-left: none; + border-radius: 0 6px 6px 0; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + flex-shrink: 0; + transition: background 0.2s; + `; + + const tabInner = document.createElement('div'); + tabInner.style.cssText = ` + color: #e0d0b0; + font-size: 10px; + `; + tabInner.textContent = 'S'; + this.tab.appendChild(tabInner); + + this.tab.addEventListener('mouseenter', () => { + this.tab.style.background = '#2a2a4e'; + }); + this.tab.addEventListener('mouseleave', () => { + this.tab.style.background = '#1a1a2e'; + }); + this.tab.addEventListener('click', () => { + this.toggle(); + }); + + this.container.appendChild(this.panel); + this.container.appendChild(this.tab); + + // Start collapsed — shift left by panel width + this.container.style.transform = 'translateY(-50%) translateX(-240px)'; + + document.body.appendChild(this.container); + } + + toggle(): void { + if (this.expanded) { + this.collapse(); + } else { + this.expand(); + } + } + + expand(): void { + this.expanded = true; + this.container.style.transform = 'translateY(-50%) translateX(0)'; + document.dispatchEvent(new CustomEvent('superlatives-panel-opened')); + } + + collapse(): void { + this.expanded = false; + this.container.style.transform = 'translateY(-50%) translateX(-240px)'; + document.dispatchEvent(new CustomEvent('superlatives-panel-closed')); + } + + isExpanded(): boolean { + return this.expanded; + } + + update(data: SuperlativesData): void { + for (const cat of CATEGORIES) { + const els = this.categoryElements.get(cat.key); + if (!els) continue; + const entry = data[cat.key]; + if (entry) { + els.nameEl.textContent = entry.name; + els.nameEl.dataset.entityId = String(entry.entityId); + els.valueEl.textContent = `(${entry.value})`; + } else { + els.nameEl.textContent = '\u2014'; + delete els.nameEl.dataset.entityId; + els.valueEl.textContent = ''; + } + } + } + + destroy(): void { + this.container.remove(); + } +}