Refine viewer status panel layout
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user