feat: add SuperlativesPanel UI component

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-03-08 04:47:22 +00:00
parent b3a74e40e1
commit 4fe41b7cf0
+237
View File
@@ -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<string, { nameEl: HTMLSpanElement; valueEl: HTMLSpanElement }> = 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();
}
}