feat(ui): map canvas follows light/dark theme; fix invisible gear control
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:
@@ -33,6 +33,7 @@ preference the store already manages.
|
||||
import { computeReachCircles } from "../../map/reach-circles";
|
||||
import { computeSelectionRing } from "../../map/selection-ring";
|
||||
import { reachStore } from "$lib/calculator/reach.svelte";
|
||||
import { theme as themeStore } from "$lib/theme/theme.svelte";
|
||||
import {
|
||||
reportToWorld,
|
||||
type HitTarget,
|
||||
@@ -44,7 +45,12 @@ preference the store already manages.
|
||||
computeHiddenPlanetNumbers,
|
||||
fingerprintHiddenPlanets,
|
||||
} from "../../map/visibility";
|
||||
import type { PrimitiveID } from "../../map/world";
|
||||
import {
|
||||
DARK_THEME,
|
||||
LIGHT_THEME,
|
||||
type PrimitiveID,
|
||||
type Theme,
|
||||
} from "../../map/world";
|
||||
import {
|
||||
ORDER_DRAFT_CONTEXT_KEY,
|
||||
OrderDraftStore,
|
||||
@@ -132,6 +138,13 @@ preference the store already manages.
|
||||
}> = [];
|
||||
let mountedTurn: number | null = null;
|
||||
let mountedGameId: string | null = null;
|
||||
// The palette the current renderer was mounted with (DARK_THEME or
|
||||
// LIGHT_THEME). A theme flip changes the singleton reference, which
|
||||
// the effect detects to drive a camera-preserving remount — Pixi
|
||||
// bakes the background colour at init and every primitive bakes its
|
||||
// colour at build, so a live re-tint is not possible on the same
|
||||
// instance.
|
||||
let mountedPalette: Theme | null = null;
|
||||
let onResize: (() => void) | null = null;
|
||||
let detachClick: (() => void) | null = null;
|
||||
let detachDebugProviders: (() => void) | null = null;
|
||||
@@ -177,6 +190,11 @@ preference the store already manages.
|
||||
// extras filter all derive from this rune.
|
||||
const toggles = store?.mapToggles;
|
||||
const gameId = store?.gameId ?? "";
|
||||
// Track the resolved app theme so the canvas follows the user's
|
||||
// light / dark choice. A flip re-keys the snapshot below and
|
||||
// triggers a camera-preserving remount with the new palette.
|
||||
const palette: Theme =
|
||||
themeStore.resolved === "light" ? LIGHT_THEME : DARK_THEME;
|
||||
if (!mounted || canvasEl === null || containerEl === null) return;
|
||||
if (status !== "ready" || !report || toggles === undefined) return;
|
||||
|
||||
@@ -245,6 +263,7 @@ preference the store already manages.
|
||||
const sameSnapshot =
|
||||
mountedTurn === report.turn &&
|
||||
mountedGameId === gameId &&
|
||||
mountedPalette === palette &&
|
||||
handle !== null;
|
||||
if (sameSnapshot) {
|
||||
// Apply wrap-mode flips in-place via the renderer's own
|
||||
@@ -274,6 +293,7 @@ preference the store already manages.
|
||||
toggles,
|
||||
hiddenPlanetNumbers,
|
||||
mode,
|
||||
palette,
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -297,6 +317,7 @@ preference the store already manages.
|
||||
extrasFingerprint,
|
||||
draftCommands,
|
||||
draftStatuses,
|
||||
palette,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -308,16 +329,22 @@ preference the store already manages.
|
||||
toggles: MapToggles,
|
||||
hiddenPlanetNumbers: ReadonlySet<number>,
|
||||
mode: "torus" | "no-wrap",
|
||||
palette: Theme,
|
||||
): import("../../map/world").Primitive[] {
|
||||
const skip = hiddenPlanetNumbers.size > 0 ? hiddenPlanetNumbers : undefined;
|
||||
const cargo = toggles.cargoRoutes
|
||||
? buildCargoRouteLines(report, skip ? { skipPlanets: skip } : undefined)
|
||||
? buildCargoRouteLines(
|
||||
report,
|
||||
skip ? { skipPlanets: skip } : undefined,
|
||||
palette,
|
||||
)
|
||||
: [];
|
||||
const pending = buildPendingSendLines(
|
||||
report,
|
||||
draftCommands,
|
||||
draftStatuses,
|
||||
skip ? { skipPlanets: skip } : undefined,
|
||||
palette,
|
||||
);
|
||||
// Reach circles published by the ship-class calculator. Empty
|
||||
// when no own planet is selected or the design is invalid, so
|
||||
@@ -331,11 +358,16 @@ preference the store already manages.
|
||||
report.mapWidth,
|
||||
report.mapHeight,
|
||||
mode,
|
||||
palette,
|
||||
)
|
||||
: [];
|
||||
const selectedPlanetId =
|
||||
selection?.selected?.kind === "planet" ? selection.selected.id : null;
|
||||
const selectionRing = computeSelectionRing(report.planets, selectedPlanetId);
|
||||
const selectionRing = computeSelectionRing(
|
||||
report.planets,
|
||||
selectedPlanetId,
|
||||
palette,
|
||||
);
|
||||
return [
|
||||
...cargo,
|
||||
...pending,
|
||||
@@ -370,10 +402,11 @@ preference the store already manages.
|
||||
extrasFingerprint: string,
|
||||
draftCommands: readonly OrderCommand[],
|
||||
draftStatuses: Readonly<Record<string, string>>,
|
||||
palette: Theme,
|
||||
): Promise<void> {
|
||||
mountInProgress = true;
|
||||
try {
|
||||
await mountRenderer(report, mode);
|
||||
await mountRenderer(report, mode, palette);
|
||||
if (handle === null) return;
|
||||
applyVisibilityState(report, toggles, hiddenPlanetNumbers);
|
||||
handle.setExtraPrimitives(
|
||||
@@ -384,6 +417,7 @@ preference the store already manages.
|
||||
toggles,
|
||||
hiddenPlanetNumbers,
|
||||
mode,
|
||||
palette,
|
||||
),
|
||||
);
|
||||
lastExtrasFingerprint = extrasFingerprint;
|
||||
@@ -426,6 +460,7 @@ preference the store already manages.
|
||||
async function mountRenderer(
|
||||
report: NonNullable<GameStateStore["report"]>,
|
||||
mode: "torus" | "no-wrap",
|
||||
palette: Theme,
|
||||
): Promise<void> {
|
||||
if (canvasEl === null || containerEl === null) return;
|
||||
// Capture camera state before disposing so a remount inside
|
||||
@@ -462,7 +497,7 @@ preference the store already manages.
|
||||
hitLookup: nextHitLookup,
|
||||
categories,
|
||||
planetDependents,
|
||||
} = reportToWorld(report);
|
||||
} = reportToWorld(report, palette);
|
||||
hitLookup = nextHitLookup;
|
||||
currentCategories = categories;
|
||||
currentPlanetDependents = planetDependents;
|
||||
@@ -471,6 +506,7 @@ preference the store already manages.
|
||||
world,
|
||||
mode,
|
||||
preference: ["webgpu", "webgl"],
|
||||
theme: palette,
|
||||
});
|
||||
const minScale = minScaleNoWrap(
|
||||
{
|
||||
@@ -592,6 +628,7 @@ preference the store already manages.
|
||||
};
|
||||
mountedTurn = report.turn;
|
||||
mountedGameId = targetGameId;
|
||||
mountedPalette = palette;
|
||||
// runSerializedMount immediately pushes the visibility
|
||||
// state + extras after this resolves; clearing the
|
||||
// fingerprint here is defensive in case the post-mount
|
||||
@@ -773,7 +810,7 @@ preference the store already manages.
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 0.4rem 0.9rem;
|
||||
background: rgba(20, 24, 42, 0.85);
|
||||
background: var(--color-surface-overlay);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
|
||||
Reference in New Issue
Block a user