feat(client): add StocksPanel UI component

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-03-09 02:55:47 +00:00
parent 803e6c9b8c
commit 3dbf5b707b
+169
View File
@@ -0,0 +1,169 @@
import type { StockpileLogEntry } from '@dflike/shared';
const MAX_LOG_ENTRIES = 50;
export class StocksPanel {
private container: HTMLDivElement;
private summaryEl: HTMLDivElement;
private logEl: HTMLDivElement;
private emptyEl: HTMLDivElement;
private logEntries: StockpileLogEntry[] = [];
private _hasUnseen = false;
onUnseenChanged: ((hasUnseen: boolean) => void) | null = null;
constructor() {
this.container = document.createElement('div');
this.container.style.cssText = `
display: flex;
flex-direction: column;
height: 100%;
gap: 8px;
`;
// Summary section
const summaryLabel = document.createElement('div');
summaryLabel.style.cssText = `
color: #e0d0b0;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1px;
padding-bottom: 4px;
border-bottom: 1px solid #8878a8;
flex-shrink: 0;
`;
summaryLabel.textContent = 'Stockpiled Resources';
this.container.appendChild(summaryLabel);
this.summaryEl = document.createElement('div');
this.summaryEl.style.cssText = `
display: flex;
flex-direction: column;
gap: 2px;
flex-shrink: 0;
padding-bottom: 6px;
border-bottom: 1px solid #8878a8;
`;
this.container.appendChild(this.summaryEl);
// Log section
const logLabel = document.createElement('div');
logLabel.style.cssText = `
color: #e0d0b0;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1px;
flex-shrink: 0;
`;
logLabel.textContent = 'Activity';
this.container.appendChild(logLabel);
this.emptyEl = document.createElement('div');
this.emptyEl.style.cssText = `
color: #8878a8;
font-size: 10px;
text-align: center;
padding: 20px 0;
`;
this.emptyEl.textContent = 'No activity yet...';
this.container.appendChild(this.emptyEl);
this.logEl = document.createElement('div');
this.logEl.style.cssText = `
display: flex;
flex-direction: column;
gap: 2px;
overflow-y: auto;
flex: 1;
`;
this.container.appendChild(this.logEl);
}
getElement(): HTMLDivElement {
return this.container;
}
get hasUnseen(): boolean {
return this._hasUnseen;
}
clearUnseen(): void {
this._hasUnseen = false;
this.onUnseenChanged?.(false);
}
updateSummary(data: Record<string, number>): void {
this.summaryEl.innerHTML = '';
const items = Object.entries(data).filter(([, qty]) => qty > 0);
if (items.length === 0) {
const empty = document.createElement('div');
empty.style.cssText = 'color: #8878a8; font-size: 10px; padding: 4px 0;';
empty.textContent = 'Empty';
this.summaryEl.appendChild(empty);
return;
}
for (const [itemId, qty] of items) {
const row = document.createElement('div');
row.style.cssText = `
display: flex;
justify-content: space-between;
font-size: 11px;
`;
const nameEl = document.createElement('span');
nameEl.style.cssText = 'color: #a0a0c0; text-transform: capitalize;';
nameEl.textContent = itemId.replace(/_/g, ' ');
const qtyEl = document.createElement('span');
qtyEl.style.cssText = 'color: #e0d0b0; text-shadow: 1px 1px 0 rgba(0,0,0,0.5);';
qtyEl.textContent = String(qty);
row.appendChild(nameEl);
row.appendChild(qtyEl);
this.summaryEl.appendChild(row);
}
}
addLogEntry(entry: StockpileLogEntry): void {
this.logEntries.push(entry);
if (this.logEntries.length > MAX_LOG_ENTRIES) {
this.logEntries.shift();
}
this.emptyEl.style.display = 'none';
const el = this.createLogLine(entry);
this.logEl.insertBefore(el, this.logEl.firstChild);
// Trim DOM if over limit
while (this.logEl.children.length > MAX_LOG_ENTRIES) {
this.logEl.removeChild(this.logEl.lastChild!);
}
this._hasUnseen = true;
this.onUnseenChanged?.(true);
}
loadHistory(entries: StockpileLogEntry[]): void {
this.logEntries = [...entries];
this.logEl.innerHTML = '';
if (entries.length === 0) {
this.emptyEl.style.display = '';
return;
}
this.emptyEl.style.display = 'none';
// Show newest first
for (let i = entries.length - 1; i >= 0; i--) {
this.logEl.appendChild(this.createLogLine(entries[i]));
}
}
private createLogLine(entry: StockpileLogEntry): HTMLDivElement {
const el = document.createElement('div');
el.style.cssText = `
font-size: 9px;
color: #a0a0c0;
padding: 2px 0;
border-bottom: 1px solid #1c1450;
line-height: 1.5;
`;
const verb = entry.action === 'dropoff' ? 'dropped off' : 'took';
const itemName = entry.itemId.replace(/_/g, ' ');
el.textContent = `${entry.npcName} ${verb} ${entry.quantity} ${itemName}`;
return el;
}
}