Refine viewer status panel layout

This commit is contained in:
2026-03-12 20:39:13 -04:00
parent 62d1c158e0
commit 0340e1cc7d
2 changed files with 42 additions and 71 deletions

View File

@@ -1,5 +1,5 @@
import * as THREE from "three";
import { fetchWorldSnapshot, openWorldStream, resetWorld } from "./api";
import { fetchWorldSnapshot, openWorldStream } from "./api";
import type {
FactionDelta,
FactionSnapshot,
@@ -86,9 +86,7 @@ export class GameViewer {
private readonly detailBodyEl: HTMLDivElement;
private readonly factionStripEl: HTMLDivElement;
private readonly networkPanelEl: HTMLDivElement;
private readonly resetButton: HTMLButtonElement;
private readonly errorEl: HTMLDivElement;
private readonly streamEl: HTMLDivElement;
private world?: WorldState;
private stream?: EventSource;
private readonly networkStats: NetworkStats = {
@@ -126,15 +124,8 @@ export class GameViewer {
hud.className = "viewer-shell";
hud.innerHTML = `
<header class="topbar">
<div>
<p class="eyebrow">Frontend Viewer</p>
<h1>Space Game Observer</h1>
</div>
<div class="topbar-actions">
<div class="status-pill">Bootstrapping</div>
<div class="status-pill stream-pill">Stream Offline</div>
<button type="button" class="reset-button">Reset World</button>
</div>
<h2>Game</h2>
<div class="topbar-body">Bootstrapping</div>
</header>
<aside class="details-panel">
<h2>Selection</h2>
@@ -149,18 +140,15 @@ export class GameViewer {
<section class="faction-strip"></section>
`;
this.statusEl = hud.querySelector(".status-pill") as HTMLDivElement;
this.streamEl = hud.querySelector(".stream-pill") as HTMLDivElement;
this.statusEl = hud.querySelector(".topbar-body") as HTMLDivElement;
this.detailTitleEl = hud.querySelector(".detail-title") as HTMLHeadingElement;
this.detailBodyEl = hud.querySelector(".detail-body") as HTMLDivElement;
this.factionStripEl = hud.querySelector(".faction-strip") as HTMLDivElement;
this.networkPanelEl = hud.querySelector(".network-body") as HTMLDivElement;
this.resetButton = hud.querySelector(".reset-button") as HTMLButtonElement;
this.errorEl = hud.querySelector(".error-strip") as HTMLDivElement;
this.container.append(this.renderer.domElement, hud);
this.resetButton.addEventListener("click", () => void this.handleReset());
this.renderer.domElement.addEventListener("pointerdown", this.onPointerDown);
this.renderer.domElement.addEventListener("pointermove", this.onPointerMove);
this.renderer.domElement.addEventListener("pointerup", this.onPointerUp);
@@ -181,14 +169,13 @@ export class GameViewer {
const snapshot = await fetchWorldSnapshot();
this.world = this.createWorldState(snapshot);
this.networkStats.snapshotBytes = new Blob([JSON.stringify(snapshot)]).size;
this.statusEl.textContent = `Snapshot ${snapshot.sequence}`;
this.updateGamePanel("Bootstrapped");
this.errorEl.hidden = true;
this.applySnapshot(snapshot);
this.openDeltaStream(snapshot.sequence);
this.updatePanels();
} catch (error) {
this.statusEl.textContent = "Backend offline";
this.streamEl.textContent = "Stream Offline";
this.updateGamePanel("Backend offline");
this.errorEl.hidden = false;
this.errorEl.textContent = error instanceof Error ? error.message : "Unable to bootstrap the backend snapshot.";
}
@@ -200,12 +187,12 @@ export class GameViewer {
onOpen: () => {
this.networkStats.streamConnected = true;
this.networkStats.streamOpenedAtMs = performance.now();
this.streamEl.textContent = "Stream Live";
this.updateGamePanel("Stream live");
this.updateNetworkPanel();
},
onError: () => {
this.networkStats.streamConnected = false;
this.streamEl.textContent = "Stream Reconnecting";
this.updateGamePanel("Stream reconnecting");
this.updateNetworkPanel();
},
onDelta: (delta, rawBytes) => {
@@ -226,26 +213,11 @@ export class GameViewer {
this.applyDelta(delta);
this.recordDeltaStats(delta, rawBytes);
this.statusEl.textContent = `Seq ${delta.sequence} · ${new Date(delta.generatedAtUtc).toLocaleTimeString()}`;
this.updateGamePanel("Live");
this.updatePanels();
this.updateNetworkPanel();
}
private async handleReset() {
this.resetButton.disabled = true;
try {
const snapshot = await resetWorld();
this.world = this.createWorldState(snapshot);
this.networkStats.snapshotBytes = new Blob([JSON.stringify(snapshot)]).size;
this.applySnapshot(snapshot);
this.openDeltaStream(snapshot.sequence);
this.updatePanels();
this.updateNetworkPanel();
} finally {
this.resetButton.disabled = false;
}
}
private createWorldState(snapshot: WorldSnapshot): WorldState {
return {
label: snapshot.label,
@@ -701,6 +673,18 @@ export class GameViewer {
].join("\n");
}
private updateGamePanel(mode: string) {
const sequence = this.world?.sequence ?? 0;
const generatedAt = this.world?.generatedAtUtc
? new Date(this.world.generatedAtUtc).toLocaleTimeString()
: "n/a";
this.statusEl.textContent = [
`mode: ${mode}`,
`sequence: ${sequence}`,
`snapshot: ${generatedAt}`,
].join("\n");
}
private toThreeVector(vector: Vector3Dto) {
return new THREE.Vector3(vector.x, vector.y, vector.z);
}

View File

@@ -51,13 +51,9 @@ canvas {
.topbar {
top: 20px;
left: 20px;
right: 20px;
width: min(360px, calc(100vw - 40px));
border-radius: 22px;
padding: 18px 20px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
pointer-events: auto;
}
@@ -70,41 +66,32 @@ canvas {
}
.topbar h1,
.topbar h2,
.details-panel h2,
.details-panel h3,
.faction-card h3 {
margin: 0;
}
.topbar h1 {
font-size: 1.2rem;
.topbar {
display: block;
align-items: start;
}
.topbar-actions {
display: flex;
align-items: center;
gap: 12px;
.topbar h2 {
color: var(--accent);
letter-spacing: 0.16em;
font-size: 0.72rem;
text-transform: uppercase;
}
.status-pill,
.reset-button {
border-radius: 999px;
border: 1px solid rgba(127, 214, 255, 0.18);
background: rgba(12, 25, 46, 0.92);
color: var(--text);
padding: 11px 16px;
font: inherit;
}
.reset-button {
cursor: pointer;
pointer-events: auto;
transition: transform 120ms ease, border-color 120ms ease;
}
.reset-button:hover {
transform: translateY(-1px);
border-color: rgba(127, 214, 255, 0.42);
.topbar-body {
margin-top: 14px;
color: var(--muted);
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
font-size: 0.8rem;
line-height: 1.6;
white-space: pre-wrap;
}
.details-panel {
@@ -120,7 +107,7 @@ canvas {
}
.network-panel {
top: 110px;
top: 172px;
left: 20px;
width: min(360px, calc(100vw - 40px));
border-radius: 24px;
@@ -228,8 +215,8 @@ canvas {
@media (max-width: 760px) {
.topbar {
flex-direction: column;
align-items: flex-start;
width: auto;
right: 20px;
}
.details-panel {
@@ -243,7 +230,7 @@ canvas {
}
.network-panel {
top: 110px;
top: 172px;
right: 20px;
left: 20px;
width: auto;