Full-screen map with draggable, collapsible floating HUD windows
- MapLibre map is now a fixed, full-viewport background layer (z-0) - New FloatingWindow: drag by title bar (pointer capture, viewport-clamped, position persisted to localStorage), collapsible to title bar, bring-to-front z-index; semi-transparent slate window with amber/cyan accents - Game screen re-laid out as floating windows over the map: slim top header (scenario, turn + date, player nation chip, Idle/Simulating status, Focus map collapse-all), dossier (top-left), event feed (right), Your move deck (bottom-center), legend (bottom-left), tiny zoom control (bottom-right) - MapLibre default nav controls removed; custom floating zoom-in/out control - Panels gain an inWindow mode (no card chrome, same testids) - server: honor HISTORY_API_PORT as PORT fallback (dev proxy/e2e agreement) - css: keep maplibre canvas base color and Radix portals above HUD windows
This commit is contained in:
+123
-104
@@ -19,6 +19,11 @@ interface Props {
|
||||
nations: Record<string, Nation>;
|
||||
/** True when this is the nation the player is leading. */
|
||||
isPlayer: boolean;
|
||||
/**
|
||||
* Render without the outer card chrome (the surrounding floating window
|
||||
* provides the border, backdrop, and title bar). Same content, same testids.
|
||||
*/
|
||||
inWindow?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,116 +33,130 @@ interface Props {
|
||||
* relation (ally = green, rival = amber, war = red, neutral = slate). Used for
|
||||
* the player (highlighted) or any inspected nation.
|
||||
*/
|
||||
export default function NationPanel({ nation, nations, isPlayer }: Props) {
|
||||
export default function NationPanel({ nation, nations, isPlayer, inWindow = false }: Props) {
|
||||
const color = nationColor(nation.id);
|
||||
const relations = Object.entries(nation.relations).sort(([a], [b]) => a.localeCompare(b));
|
||||
const regions = nation.territory;
|
||||
|
||||
const identity = (
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className="inline-block size-4 shrink-0 rounded-md ring-2 ring-white/10"
|
||||
style={{ background: color }}
|
||||
data-testid="nation-color-chip"
|
||||
/>
|
||||
<div className="min-w-0 flex-1 space-y-0.5">
|
||||
<CardTitle className="truncate text-lg">{nation.name}</CardTitle>
|
||||
<CardDescription>
|
||||
{nation.type} · {regions.length} region{regions.length === 1 ? '' : 's'}
|
||||
</CardDescription>
|
||||
</div>
|
||||
{isPlayer && (
|
||||
<Badge variant="default" className="shrink-0">
|
||||
Your nation
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const sections = (
|
||||
<div className="space-y-5">
|
||||
<section>
|
||||
<h3 className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Status
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<StatBar
|
||||
label="Military"
|
||||
value={nation.stats.military}
|
||||
testId={isPlayer ? 'stat-military' : undefined}
|
||||
/>
|
||||
<StatBar
|
||||
label="Economy"
|
||||
value={nation.stats.economy}
|
||||
testId={isPlayer ? 'stat-economy' : undefined}
|
||||
/>
|
||||
<StatBar
|
||||
label="Stability"
|
||||
value={nation.stats.stability}
|
||||
testId={isPlayer ? 'stat-stability' : undefined}
|
||||
/>
|
||||
<StatBar
|
||||
label="Population"
|
||||
value={nation.stats.population}
|
||||
testId={isPlayer ? 'stat-population' : undefined}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Territory
|
||||
</h3>
|
||||
{regions.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{regions.map((code) => (
|
||||
<span
|
||||
key={code}
|
||||
className="rounded border px-1.5 py-0.5 text-xs tabular-nums"
|
||||
style={{ borderColor: `${color}66`, color, background: `${color}12` }}
|
||||
>
|
||||
{code}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No territory.</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Diplomacy
|
||||
</h3>
|
||||
{relations.length > 0 ? (
|
||||
<ul className="space-y-1.5" data-testid="relations-list">
|
||||
{relations.map(([id, status]) => {
|
||||
const other = nations[id];
|
||||
const c = RELATION_COLORS[status] ?? '#94a3b8';
|
||||
return (
|
||||
<li
|
||||
key={id}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
data-testid="relation-row"
|
||||
data-status={status}
|
||||
>
|
||||
<span className="inline-block size-2.5 shrink-0 rounded-full" style={{ background: c }} />
|
||||
<span className="min-w-0 flex-1 truncate text-foreground/85">
|
||||
{other?.name ?? id}
|
||||
</span>
|
||||
<span className="shrink-0 text-xs font-medium" style={{ color: c }}>
|
||||
{RELATION_LABEL[status] ?? status}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No diplomatic relations yet.</p>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (inWindow) {
|
||||
return (
|
||||
<div data-testid="nation-panel" className="space-y-4 p-4">
|
||||
{identity}
|
||||
{sections}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card data-testid="nation-panel" className={isPlayer ? 'border-primary/40' : undefined}>
|
||||
<CardHeader className="space-y-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className="inline-block size-4 shrink-0 rounded-md ring-2 ring-white/10"
|
||||
style={{ background: color }}
|
||||
data-testid="nation-color-chip"
|
||||
/>
|
||||
<div className="min-w-0 flex-1 space-y-0.5">
|
||||
<CardTitle className="truncate text-lg">{nation.name}</CardTitle>
|
||||
<CardDescription>
|
||||
{nation.type} · {regions.length} region{regions.length === 1 ? '' : 's'}
|
||||
</CardDescription>
|
||||
</div>
|
||||
{isPlayer && (
|
||||
<Badge variant="default" className="shrink-0">
|
||||
Your nation
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-5">
|
||||
<section>
|
||||
<h3 className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Status
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<StatBar
|
||||
label="Military"
|
||||
value={nation.stats.military}
|
||||
testId={isPlayer ? 'stat-military' : undefined}
|
||||
/>
|
||||
<StatBar
|
||||
label="Economy"
|
||||
value={nation.stats.economy}
|
||||
testId={isPlayer ? 'stat-economy' : undefined}
|
||||
/>
|
||||
<StatBar
|
||||
label="Stability"
|
||||
value={nation.stats.stability}
|
||||
testId={isPlayer ? 'stat-stability' : undefined}
|
||||
/>
|
||||
<StatBar
|
||||
label="Population"
|
||||
value={nation.stats.population}
|
||||
testId={isPlayer ? 'stat-population' : undefined}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Territory
|
||||
</h3>
|
||||
{regions.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{regions.map((code) => (
|
||||
<span
|
||||
key={code}
|
||||
className="rounded border px-1.5 py-0.5 text-xs tabular-nums"
|
||||
style={{ borderColor: `${color}66`, color, background: `${color}12` }}
|
||||
>
|
||||
{code}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No territory.</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Diplomacy
|
||||
</h3>
|
||||
{relations.length > 0 ? (
|
||||
<ul className="space-y-1.5" data-testid="relations-list">
|
||||
{relations.map(([id, status]) => {
|
||||
const other = nations[id];
|
||||
const c = RELATION_COLORS[status] ?? '#94a3b8';
|
||||
return (
|
||||
<li
|
||||
key={id}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
data-testid="relation-row"
|
||||
data-status={status}
|
||||
>
|
||||
<span className="inline-block size-2.5 shrink-0 rounded-full" style={{ background: c }} />
|
||||
<span className="min-w-0 flex-1 truncate text-foreground/85">
|
||||
{other?.name ?? id}
|
||||
</span>
|
||||
<span className="shrink-0 text-xs font-medium" style={{ color: c }}>
|
||||
{RELATION_LABEL[status] ?? status}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No diplomatic relations yet.</p>
|
||||
)}
|
||||
</section>
|
||||
</CardContent>
|
||||
<CardHeader className="space-y-0">{identity}</CardHeader>
|
||||
<CardContent>{sections}</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user