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