feat(ui): map canvas follows light/dark theme; fix invisible gear control
Tests · UI / test (push) Waiting to run
Tests · UI / test (pull_request) Successful in 2m45s

The map view now selects a DARK_THEME or LIGHT_THEME palette from the
resolved app theme and threads it through every primitive builder, so
the canvas, planets, ship groups, cargo routes, battle/bombing markers,
fog, reach + selection rings, pending-Send tracks, and the pick overlay
all switch with the rest of the chrome. A theme flip remounts the
renderer preserving the camera — Pixi bakes the background at init and
every primitive bakes its colour at build, so a live re-tint is not
possible on the same instance.

This also fixes the reported bug: the gear-popover trigger and the
loading overlay hardcoded a dark navy background, so in light theme the
gear was invisible (dark icon on dark chip) until hover flipped it to a
white chip. Both now use the --color-surface-overlay token and read
correctly in both themes.

The light palette mirrors the dark one role-for-role, darkened /
saturated for contrast on a light background while keeping the incoming,
battle, and bombing accents vivid. The values are a first pass meant to
be refined during the F8 manual-QA loop.

Removes the now-dead "Phase 35" references from the code and lifts the
map-recoloring prohibition from the design-system / renderer docs; the
battle scene stays a fixed-palette data-viz surface.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-24 08:49:37 +02:00
parent d44ad9b6eb
commit f6e4a4f6bd
27 changed files with 631 additions and 230 deletions
+22 -31
View File
@@ -2,8 +2,8 @@
// short arrow from the source planet to its destination, drawn as
// three `LinePrim` segments — one shaft and two arrowhead wings —
// styled per load type so the four cargo kinds are
// distinguishable at a glance. Phase 16 ships placeholder
// colours; Phase 35 polish picks final values.
// distinguishable at a glance. The stroke colours come from the
// active `Theme` (dark or light); the alpha and width are fixed.
//
// Geometry uses `torusShortestDelta` so an arrow that crosses the
// torus seam takes the wrap, not the long way round, matching the
@@ -13,35 +13,21 @@
import type { GameReport, ReportPlanet } from "../api/game-state";
import type { CargoLoadType } from "../sync/order-types";
import { torusShortestDelta } from "./math";
import type { LinePrim, PrimitiveID, Style } from "./world";
import { DARK_THEME, type LinePrim, type PrimitiveID, type Style, type Theme } from "./world";
export const STYLE_ROUTE_COL: Style = {
strokeColor: 0x4fc3f7,
strokeAlpha: 0.95,
strokeWidthPx: 2,
};
export const STYLE_ROUTE_CAP: Style = {
strokeColor: 0xffb74d,
strokeAlpha: 0.95,
strokeWidthPx: 2,
};
export const STYLE_ROUTE_MAT: Style = {
strokeColor: 0x81c784,
strokeAlpha: 0.95,
strokeWidthPx: 2,
};
export const STYLE_ROUTE_EMP: Style = {
strokeColor: 0x90a4ae,
strokeAlpha: 0.85,
strokeWidthPx: 1,
};
const STYLE_BY_LOAD_TYPE: Record<CargoLoadType, Style> = {
COL: STYLE_ROUTE_COL,
CAP: STYLE_ROUTE_CAP,
MAT: STYLE_ROUTE_MAT,
EMP: STYLE_ROUTE_EMP,
};
/**
* routeStylesByLoadType builds the per-load-type stroke styles for the
* active theme. A single `Style` object is shared by every line of a
* given load type within one call so the renderer can dedupe them.
*/
function routeStylesByLoadType(theme: Theme): Record<CargoLoadType, Style> {
return {
COL: { strokeColor: theme.routeCol, strokeAlpha: 0.95, strokeWidthPx: 2 },
CAP: { strokeColor: theme.routeCap, strokeAlpha: 0.95, strokeWidthPx: 2 },
MAT: { strokeColor: theme.routeMat, strokeAlpha: 0.95, strokeWidthPx: 2 },
EMP: { strokeColor: theme.routeEmp, strokeAlpha: 0.85, strokeWidthPx: 1 },
};
}
/** Per-load-type priority. Higher wins hit-test ties; planets sit
* at 1..4 (`state-binding.ts.priorityFor`), so route arrows always
@@ -91,13 +77,18 @@ const HEAD_HALF_ANGLE = (25 * Math.PI) / 180;
* whose routes — outgoing or incoming — should be filtered out so the
* arrows do not point at hidden glyphs. Empty / undefined means no
* extra filtering, preserving the pre-Phase-29 contract.
*
* `theme` supplies the per-load-type stroke colours and defaults to
* `DARK_THEME`.
*/
export function buildCargoRouteLines(
report: GameReport,
opts?: { skipPlanets?: ReadonlySet<number> },
theme: Theme = DARK_THEME,
): LinePrim[] {
if (report.routes.length === 0) return [];
const skip = opts?.skipPlanets;
const styleByLoadType = routeStylesByLoadType(theme);
const planetById = new Map<number, ReportPlanet>();
for (const planet of report.planets) {
planetById.set(planet.number, planet);
@@ -131,7 +122,7 @@ export function buildCargoRouteLines(
route.sourcePlanetNumber,
entry.loadType,
);
const style = STYLE_BY_LOAD_TYPE[entry.loadType];
const style = styleByLoadType[entry.loadType];
const priority = PRIORITY_BY_LOAD_TYPE[entry.loadType];
lines.push({
kind: "line",