feat: add SuperlativesPanel UI component
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user