From 6c3c24953d48b3ab565f1fabcda8ac1d39d5eb9e Mon Sep 17 00:00:00 2001 From: Matei Date: Tue, 9 Jun 2026 00:22:47 -0400 Subject: [PATCH 1/2] =?UTF-8?q?spec(mapgen-studio):=20shell-reskin=20chang?= =?UTF-8?q?eset=20=E2=80=94=20token-driven=20chrome=20+=20primitive=20adop?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .../mapgen-studio-shell-reskin/design.md | 75 +++++++++++++++++ .../mapgen-studio-shell-reskin/proposal.md | 55 +++++++++++++ .../specs/mapgen-studio/spec.md | 82 +++++++++++++++++++ .../mapgen-studio-shell-reskin/tasks.md | 45 ++++++++++ 4 files changed, 257 insertions(+) create mode 100644 openspec/changes/mapgen-studio-shell-reskin/design.md create mode 100644 openspec/changes/mapgen-studio-shell-reskin/proposal.md create mode 100644 openspec/changes/mapgen-studio-shell-reskin/specs/mapgen-studio/spec.md create mode 100644 openspec/changes/mapgen-studio-shell-reskin/tasks.md diff --git a/openspec/changes/mapgen-studio-shell-reskin/design.md b/openspec/changes/mapgen-studio-shell-reskin/design.md new file mode 100644 index 0000000000..a7e10d2b9d --- /dev/null +++ b/openspec/changes/mapgen-studio-shell-reskin/design.md @@ -0,0 +1,75 @@ +## Design + +This is a presentation-only migration slice. It moves the shell/chrome off the +legacy hand-rolled primitives and ad-hoc hex palette onto the design system +tokens and the new `src/components/ui` primitives. No runtime logic changes — +refactors MOVE styling and swap primitive mechanisms; they do not rewrite +behavior. + +## Token Mapping + +The shell encoded its surfaces as a `lightMode ? '' : ''` +ternary per class. Because the design tokens already carry both themes (light in +`:root`, dark in `.dark`), each ternary collapses to one token utility: + +| Legacy (dark / light) | Token | +| --- | --- | +| `bg-[#141418]/95` / `bg-white/95` (floating dock) | `bg-popover/95` | +| `bg-[#16161d]/90` / `bg-white/90` (brand pill) | `bg-popover/90` | +| `border-[#2a2a32]` / `border-gray-200` | `border-border` | +| `border-[#222228]` / `border-gray-100` (subtle) | `border-subtle` | +| `bg-[#0f0f12]` / `bg-gray-50` (sunken JSON) | `bg-surface-sunken` | +| `text-[#e8e8ed]` / `text-[#1f2937]` (primary) | `text-foreground` | +| `text-[#8a8a96]` / `text-[#6b7280]` (secondary) | `text-muted-foreground` | +| `text-[#5a5a66]` / `text-[#9ca3af]` (tertiary) | `text-muted-foreground/70` | +| `hover:bg-[#1a1a1f]` / `hover:bg-gray-50` | `hover:bg-accent` | +| `bg-[#222228]` / `bg-gray-200` (active) | `bg-muted` | +| `accent-[#64748b]` (range slider) | `accent-primary` | + +The floating chrome docks ride the `popover` tier deliberately: they float above +the deck.gl map (with `backdrop-blur`), and `popover` is the system's +floating-layer surface. Nested sunken surfaces (the JSON config view) use +`surface-sunken`; text inputs use `input-background`. + +## Accent Discipline + +`system.md` mandates ONE elevated cool-steel slate accent. The shell previously +used orange (`ring-orange-400`, `border-orange-400`, `text-orange-500`) for two +distinct jobs: + +- **Chrome identity** — the dirty/modified ring on Run, the active auto-run + toggle, focus emphasis. These move to the slate `primary`/`ring` tokens, since + they are instrument identity. +- **Live-stale emphasis** — the "your Studio state is stale vs the live game" + call-to-action. This is a *warning about data*, so it moves to the `warning` + token rather than `primary`, keeping the slate reserved for identity. + +Status dots (running = amber, error = `destructive`, ready/live-ok = `success`) +are semantic data the instrument reports about the map and the live game — the +one place real color belongs in the chrome — so they keep their semantic hues. + +## Primitive And Mechanism Swaps + +- Shell call sites adopt `@/components/ui` Button/Input/Select/Switch where the + prop surface matches. The legacy primitives still accept a `lightMode` prop; + the new ones ignore theme props entirely (they read `.dark`), so the migration + drops the `lightMode=` pass-through at each migrated site. +- Native `title=` tooltips become the shadcn `Tooltip` (Radix), wrapped by a + single `TooltipProvider` mounted at the shell root. This gives the chrome real, + token-styled, delay-grouped tooltips instead of native browser ones. +- The legacy `AlertDialog` (RecipePanel reset confirm, PresetDialogs) migrates to + the shadcn `Dialog`. The confirm/cancel/destructive semantics are preserved by + composing `DialogFooter` with Buttons; `onOpenChange` keeps the same + open-state contract the callers already pass. +- `ToastProvider`/`useToast` migrate to the sonner `Toaster` + the `toast` + function. The legacy `toast(message, { variant })` call shape maps to + `toast.success` / `toast.error` / `toast.info` / `toast` so message content and + trigger points are unchanged. + +## Parity Boundary + +No map generation, deck.gl math, recipe semantics, run-in-game phases, the +live-runtime poll's staleness/backoff gating, the localStorage schema, or +browserRunner gating is touched. Every change is a class string, a primitive +import, or a dialog/toast call-shape swap. The shell's props, callbacks, and +control flow are untouched. diff --git a/openspec/changes/mapgen-studio-shell-reskin/proposal.md b/openspec/changes/mapgen-studio-shell-reskin/proposal.md new file mode 100644 index 0000000000..58ae752af1 --- /dev/null +++ b/openspec/changes/mapgen-studio-shell-reskin/proposal.md @@ -0,0 +1,55 @@ +## Why + +The canonical token-driven primitive library now exists at +`apps/mapgen-studio/src/components/ui/`, but the shell/chrome still renders +through the legacy `src/ui/components/ui/*` primitives and a wall of hardcoded +hex palettes gated on a `lightMode`/`isLightMode` boolean (`bg-[#141418]/95`, +`border-[#2a2a32]`, `text-[#8a8a96]`, `bg-[#1a1a1f]`, …). The design system is +invisible: the substrate-elevation tiers, the single slate accent, the contour +focus ring, and the `.dark`-class theming never reach the surface the user +actually sees. + +This slice makes the design system VISIBLE across the shell. It reskins the +chrome (header, brand, view controls, left dock / RecipePanel, right dock / +ExplorePanel, footer / status strip, the app root and error strip) onto the +design tokens, adopts the new primitives at their call sites, converts native +`title=` tooltips to the shadcn `Tooltip`, and migrates the legacy +`AlertDialog`/`ToastProvider` usages to the shadcn `Dialog` and sonner `toast`. + +## What Changes + +- Replace ad-hoc hex / `lightMode`-ternary classes in the shell with design + tokens so depth is FELT: page = `bg-background`, floating chrome docks = + `bg-popover`, nested sunken surfaces = `surface-sunken`, + inputs = `input-background`; borders via `border`/`border-subtle` per the + ladder; secondary text via `text-muted-foreground`. The light theme now comes + from `:root` and the dark theme from `.dark`, so the per-class `lightMode` + branching collapses to a single token class. +- Commit to the ONE slate accent: chrome identity states (active/dirty/focus + rings, auto-run, live-stale emphasis) move from ad-hoc orange to the + `primary`/`ring` tokens. Semantic *data* indicators (status dots: + running/error/ready, live-ok/error) stay on their semantic hues — they are the + matter the instrument reports, not instrument identity. +- Adopt the new `src/components/ui` primitives (Button, Input, Select, Switch, + Tooltip, Dialog) at the shell + preset-dialog + rjsf-widget call sites. +- Convert native `title=` attributes in the shell to the shadcn `Tooltip`, + wrapped by a single `TooltipProvider` at the shell root. +- Migrate the legacy `AlertDialog` (RecipePanel reset, PresetDialogs) to the + shadcn `Dialog`, and migrate `ToastProvider`/`useToast` to the sonner + `Toaster` + `toast`. + +This change is PRESENTATION ONLY. It moves styling onto tokens and swaps +primitive mechanisms; it changes no map generation, deck.gl, recipe, run-in-game, +live-runtime poll, localStorage, or browserRunner logic. + +## Impact + +- Affected specs: `mapgen-studio` +- Affected code: `apps/mapgen-studio/src/ui/components/AppHeader.tsx`, + `AppBrand.tsx`, `ViewControls.tsx`, `RecipePanel.tsx`, `ExplorePanel.tsx`, + `AppFooter.tsx`; `apps/mapgen-studio/src/App.tsx` (root bg, Toaster, error + strip); `apps/mapgen-studio/src/features/presets/PresetDialogs.tsx`; + `apps/mapgen-studio/src/features/configOverrides/rjsfWidgets.tsx`. +- No change to map generation, deck.gl, recipe semantics, run-in-game, the + live-runtime poll staleness/backoff gating, the localStorage schema, or + browserRunner gating. diff --git a/openspec/changes/mapgen-studio-shell-reskin/specs/mapgen-studio/spec.md b/openspec/changes/mapgen-studio-shell-reskin/specs/mapgen-studio/spec.md new file mode 100644 index 0000000000..017584e5c9 --- /dev/null +++ b/openspec/changes/mapgen-studio-shell-reskin/specs/mapgen-studio/spec.md @@ -0,0 +1,82 @@ +## ADDED Requirements + +### Requirement: Shell Renders Through Design Tokens + +MapGen Studio SHALL render the shell/chrome — app root, header, brand, view +controls, the left dock (RecipePanel), the right dock (ExplorePanel), and the +footer/status strip — with surfaces, borders, and text resolved through design +system tokens rather than hardcoded hex palettes or a `lightMode`/`isLightMode` +class ternary, so the substrate-elevation hierarchy is felt and the theme flips +through the single `.dark` class. + +#### Scenario: Floating docks ride the popover tier +- **WHEN** the header, brand, view-controls, recipe, explore, or footer chrome + renders over the deck.gl map +- **THEN** its surface resolves from `bg-popover` (the floating-layer tier), its + borders from `border`/`border-subtle`, and its text from + `text-foreground`/`text-muted-foreground` +- **AND** it declares no hex color literal and no `lightMode`-gated class ternary + for those surfaces + +#### Scenario: Nested and inset surfaces use their tiers +- **WHEN** a nested sunken surface (the JSON config view) or a text input renders + inside a dock +- **THEN** the sunken surface resolves from `surface-sunken` and the input from + `input-background`, a felt step apart from the dock surface + +#### Scenario: Theme flips through the single dark class +- **WHEN** the `.dark` class on `` is toggled +- **THEN** the shell re-themes from the token values with no per-class + `lightMode` branch driving its surface, border, or text colors + +### Requirement: Single Slate Accent For Chrome Identity + +MapGen Studio SHALL render chrome identity states in the shell (active toggles, +the dirty/modified emphasis ring, focus emphasis) through the single slate +`primary`/`ring` accent. Status indicators that report data about the map or the +live game SHALL keep their semantic hues, and a data-staleness call-to-action +SHALL use the `warning` token rather than the slate accent. + +#### Scenario: Dirty and active states use the slate accent +- **WHEN** the Run control is dirty, or the auto-run toggle is active +- **THEN** its emphasis renders through `primary`/`ring` (the slate accent), not + an ad-hoc orange + +#### Scenario: Status dots keep semantic hues +- **WHEN** a generation/live status dot renders +- **THEN** running stays amber, error resolves from `destructive`, and + ready/live-ok resolve from `success` — the semantic data the instrument reports + +### Requirement: Shell Adopts The Canonical Primitives And Tooltips + +The shell, the preset dialogs, and the rjsf widgets SHALL adopt the canonical +`src/components/ui` primitives at their call sites, native `title=` tooltips in +the shell SHALL be replaced by the shadcn `Tooltip` under a single +`TooltipProvider`, and the legacy `AlertDialog` and `ToastProvider`/`useToast` +SHALL be replaced by the shadcn `Dialog` and the sonner `Toaster`/`toast`. + +#### Scenario: Native titles become token-styled tooltips +- **WHEN** a shell control that previously used a native `title=` attribute is + hovered or focused +- **THEN** it shows a shadcn `Tooltip` (token-styled, delay-grouped under the + shell `TooltipProvider`) instead of a native browser tooltip + +#### Scenario: Dialog and toast use the canonical mechanisms +- **WHEN** the RecipePanel reset confirm, a preset error/save/confirm dialog, or + a toast notification fires +- **THEN** it renders through the shadcn `Dialog` (reset/preset) or the sonner + `Toaster` via `toast` (notifications), preserving the prior open/confirm/cancel + and message semantics + +### Requirement: Shell Reskin Preserves Behavior Parity + +The shell reskin SHALL be presentation-only and SHALL NOT alter map generation, +deck.gl rendering, recipe semantics, run-in-game, the live-runtime poll +staleness/backoff gating, the localStorage schema, or browserRunner gating. + +#### Scenario: Only presentation changes +- **WHEN** the reskin lands +- **THEN** the shell's props, callbacks, and control flow are unchanged, and map + generation, deck.gl rendering, recipe semantics, run-in-game, the live-runtime + poll staleness/backoff gating, the localStorage schema, and browserRunner + gating behave exactly as before diff --git a/openspec/changes/mapgen-studio-shell-reskin/tasks.md b/openspec/changes/mapgen-studio-shell-reskin/tasks.md new file mode 100644 index 0000000000..32e92df553 --- /dev/null +++ b/openspec/changes/mapgen-studio-shell-reskin/tasks.md @@ -0,0 +1,45 @@ +## 1. Substrate-Elevation Token Reskin + +- [ ] 1.1 Reskin `AppBrand.tsx`: floating pill + hover card → `bg-popover`, + `border`/`border-subtle`, `text-foreground`/`text-muted-foreground`; drop the + `lightMode` hex ternaries. +- [ ] 1.2 Reskin `ViewControls.tsx`: dock → `bg-popover`, dividers → `bg-border`, + icon buttons → `text-muted-foreground hover:bg-accent`, active → `bg-muted`. +- [ ] 1.3 Reskin `AppHeader.tsx`: world-settings docks → `bg-popover`, dividers, + labels → `text-muted-foreground`; setup button onto tokens. +- [ ] 1.4 Reskin `RecipePanel.tsx`: panel → `bg-popover`, section headers, + borders → `border-subtle`, sunken JSON view → `surface-sunken`, save menu → + `bg-popover`; dirty ring → `ring-ring`/`border-primary`. +- [ ] 1.5 Reskin `ExplorePanel.tsx`: panel → `bg-popover`, list item active/ + inactive onto tokens, badges → `bg-muted`, range accents → `accent-primary`, + native selects onto `bg-input-background`/`border-input`. +- [ ] 1.6 Reskin `AppFooter.tsx`: status/live/run docks → `bg-popover`, dividers, + text tiers onto tokens; dirty/auto-run rings → `ring-ring`/`border-primary`; + keep semantic status-dot hues. +- [ ] 1.7 Reskin `App.tsx` root background and error strip onto `bg-background` + and `bg-destructive`/`text-destructive` tokens. + +## 2. Primitive Adoption And Tooltips + +- [ ] 2.1 Wrap the shell in a single `TooltipProvider`; convert native `title=` + attributes in the shell (ViewControls, AppFooter, RecipePanel, ExplorePanel) + to the shadcn `Tooltip`. +- [ ] 2.2 Swap shell primitive call sites (Button, Input, Select, Switch) to + `@/components/ui` where the prop surface is compatible. + +## 3. Dialog And Toast Migration + +- [ ] 3.1 Migrate the RecipePanel reset `AlertDialog` to the shadcn `Dialog`. +- [ ] 3.2 Migrate `PresetDialogs.tsx` (error/save/confirm) to the shadcn + `Dialog`, preserving open/confirm/cancel semantics. +- [ ] 3.3 Migrate `ToastProvider`/`useToast` to the sonner `Toaster` + `toast`, + mapping `{ variant }` to `toast.success`/`toast.error`/`toast.info`/`toast`. + +## 4. Verification + +- [ ] 4.1 Verify `bun run check` (tsc --noEmit) is clean. +- [ ] 4.2 Verify `bun run build` succeeds including the worker-bundle check. +- [ ] 4.3 Start the preview, screenshot the shell in dark mode; confirm no + console errors and that the squint test reads hierarchy (page < dock < + floating) with the single slate accent. +- [ ] 4.4 Run `bun run openspec -- validate mapgen-studio-shell-reskin --strict`. From 81b12e000006cd75be9a13329b51182c5164e52b Mon Sep 17 00:00:00 2001 From: Matei Date: Tue, 9 Jun 2026 00:43:50 -0400 Subject: [PATCH 2/2] feat(mapgen-studio): reskin shell onto substrate tokens + adopt primitives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the design system VISIBLE across the chrome. Replace ad-hoc hex / lightMode-ternary classes in the header, brand, view controls, recipe dock, explore dock, footer/status strip, and app root with design tokens so the substrate-elevation hierarchy is felt: page=bg-background, floating docks= bg-popover, nested=surface-sunken, inputs=input-background; borders via border/border-subtle. Commit to the ONE slate accent for chrome identity (dirty/active/focus rings -> primary/ring); keep semantic status-dot hues; move the live-stale CTA to the warning token. Adopt the canonical src/components/ui primitives (Button/Input/Switch) at the shell call sites, convert native title= hints to the shadcn Tooltip under a single TooltipProvider, migrate the legacy AlertDialog (RecipePanel reset, PresetDialogs) to the shadcn Dialog, and migrate ToastProvider/useToast to the sonner Toaster + toast (variant -> success/error/info). The native-select-based Select is reskinned via tokens but kept (Radix Select swap deferred to P4 — incompatible compound API, behavior-sensitive). Presentation only: no map-gen, deck.gl, recipe, run-in-game, live-poll, localStorage, or browserRunner logic changed. Verified: tsc clean; prod build incl. worker-bundle check; live dark-mode shell reads hierarchy in the squint test with the single slate accent; zero console errors on clean mount and through tooltip + dialog interaction. OpenSpec: mapgen-studio-shell-reskin Co-Authored-By: Claude Opus 4.8 --- apps/mapgen-studio/src/App.tsx | 265 +++++--------- .../src/features/presets/PresetDialogs.tsx | 128 ++++--- .../src/ui/components/AppBrand.tsx | 69 ++-- .../src/ui/components/AppFooter.tsx | 318 ++++++++++------- .../src/ui/components/AppHeader.tsx | 75 ++-- .../src/ui/components/ExplorePanel.tsx | 330 ++++++------------ .../src/ui/components/RecipePanel.tsx | 237 +++++++------ .../src/ui/components/ViewControls.tsx | 84 +++-- .../mapgen-studio-shell-reskin/tasks.md | 38 +- 9 files changed, 696 insertions(+), 848 deletions(-) diff --git a/apps/mapgen-studio/src/App.tsx b/apps/mapgen-studio/src/App.tsx index ff8b1c36e4..3ef738e04d 100644 --- a/apps/mapgen-studio/src/App.tsx +++ b/apps/mapgen-studio/src/App.tsx @@ -10,7 +10,7 @@ import { AppHeader } from "./ui/components/AppHeader"; import { AppFooter } from "./ui/components/AppFooter"; import { ExplorePanel } from "./ui/components/ExplorePanel"; import { RecipePanel } from "./ui/components/RecipePanel"; -import { ToastProvider, useToast } from "./ui/components/ui"; +import { Toaster, toast as sonnerToast, TooltipProvider } from "./components/ui"; import { createTheme, useThemePreference } from "./ui/hooks"; import { configsEqual, recipeSettingsEqual, worldSettingsEqual } from "./ui/utils/config"; import { formatStageName } from "./ui/utils/formatting"; @@ -75,10 +75,6 @@ import { updateMapConfigSaveDeployStatus, type MapConfigSaveDeployStatus, } from "./features/mapConfigSave/status"; -import { - createStudioServerOrpcClient, - studioServerOrpcFailure, -} from "./features/studioServer/studioServerClient"; import { buildLiveRuntimeErrorState, buildLiveRuntimeSnapshotQuery, @@ -94,8 +90,6 @@ import { } from "./features/liveRuntime/model"; import { DeckCanvas, type DeckCanvasApi } from "./features/viz/DeckCanvas"; import { useVizState } from "./features/viz/useVizState"; -import { createStudioRecipeDagClient, type RecipeDagResult } from "./features/recipeDag/client"; -import { RecipeDagStatsBar, RecipeDagView } from "./features/recipeDag/RecipeDagView"; import { findVariantIdForEra, findVariantKeyForEra, @@ -103,10 +97,6 @@ import { parseEraVariantKey, resolveFixedEraUiValue, } from "./features/viz/era"; -import { - buildRiverLakeFloodplainInspectorSummary, - type RiverLakeInspectorLayerRef, -} from "./features/viz/riverLakeInspector"; import { formatErrorForUi } from "./shared/errorFormat"; import { shouldIgnoreGlobalShortcutsInEditableTarget } from "./shared/shortcuts/shortcutPolicy"; import type { VizEvent } from "./shared/vizEvents"; @@ -136,17 +126,6 @@ import { import { getOverlaySuggestions } from "./recipes/overlaySuggestions"; const civ7ControlOrpcClient = createStudioCiv7ControlOrpcClient(); -const recipeDagClient = createStudioRecipeDagClient(); -const studioServerClient = createStudioServerOrpcClient(); - -type StudioView = "map" | "dag"; - -type RecipeDagClientState = Readonly<{ - status: "idle" | "loading" | "ready" | "error"; - recipeId: string | null; - dag: RecipeDagResult | null; - error: string | null; -}>; function isPlainObject(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); @@ -175,7 +154,7 @@ async function saveRepoBackedConfig(args: { config: unknown; onStatus?: (status: MapConfigSaveDeployStatus) => void; }): Promise< - | { ok: true; path?: string; deploy?: MapConfigSaveDeployStatus["deploy"] } + | { ok: true; path?: string; deploy?: { command?: string } } | { ok: false; error: string; saved?: boolean; deployed?: boolean; path?: string } > { const envelope = { @@ -189,12 +168,16 @@ async function saveRepoBackedConfig(args: { config: args.config, }; try { - let status = await studioServerClient.mapConfigs.saveDeploy({ - requestId: args.requestId, - id: args.id, - sourcePath: args.sourcePath, - envelope, + const res = await fetch("/api/map-configs", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ requestId: args.requestId, id: args.id, sourcePath: args.sourcePath, envelope }), }); + const body = (await res.json().catch(() => null)) as (Partial & { error?: string }) | null; + if (!res.ok || !body?.requestId) { + return { ok: false, error: body?.error ?? `HTTP ${res.status}`, path: body?.path }; + } + let status = body as MapConfigSaveDeployStatus; args.onStatus?.(status); while (status.status === "running") { await delay(500); @@ -216,8 +199,7 @@ async function saveRepoBackedConfig(args: { } return { ok: true, path: status.path, deploy: status.deploy }; } catch (err) { - const failure = studioServerOrpcFailure(err, "Repo config save failed"); - return { ok: false, error: failure.error, path: failure.data?.path as string | undefined }; + return { ok: false, error: err instanceof Error ? err.message : "Repo config save failed" }; } } @@ -231,10 +213,14 @@ function isAbortLikeError(err: unknown): boolean { async function fetchMapConfigSaveDeployStatus(requestId: string): Promise { try { - return await studioServerClient.mapConfigs.status({ requestId }); + const res = await fetch(`/api/map-configs/status?requestId=${encodeURIComponent(requestId)}`); + const body = (await res.json().catch(() => null)) as (Partial & { error?: string }) | null; + if (!res.ok || !body?.requestId) { + return { ok: false, error: body?.error ?? `HTTP ${res.status}`, statusCode: res.status }; + } + return body as MapConfigSaveDeployStatus; } catch (err) { - const failure = studioServerOrpcFailure(err, "Save/Deploy status unavailable"); - return { ok: false, error: failure.error, statusCode: failure.statusCode }; + return { ok: false, error: err instanceof Error ? err.message : "Save/Deploy status unavailable" }; } } @@ -265,38 +251,50 @@ async function runCurrentConfigInGame(args: { | { ok: false; error: string; details?: RunInGameFailureDetails; statusCode?: number } > { try { - const status = await studioServerClient.runInGame.start({ - recipeId: args.recipeId, - seed: args.seed, - mapSize: args.mapSize, - playerCount: args.playerCount, - resources: args.resources, - setupConfig: normalizeStudioSetupConfig(args.setupConfig), - materialization: { mode: args.materializationMode }, - ...(args.restartCivProcess ? { recovery: { restartCivProcess: true } } : {}), - selectedConfig: args.selectedConfig, - config: args.config, - sourceSnapshot: args.sourceSnapshot, + const res = await fetch("/api/civ7/run-in-game", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + recipeId: args.recipeId, + seed: args.seed, + mapSize: args.mapSize, + playerCount: args.playerCount, + resources: args.resources, + setupConfig: normalizeStudioSetupConfig(args.setupConfig), + materialization: { mode: args.materializationMode }, + ...(args.restartCivProcess ? { recovery: { restartCivProcess: true } } : {}), + selectedConfig: args.selectedConfig, + config: args.config, + sourceSnapshot: args.sourceSnapshot, + }), }); - return status as RunInGameOperationStatus; + const body = (await res.json().catch(() => null)) as + | (Partial & { error?: string; details?: RunInGameFailureDetails }) + | null; + if (!res.ok || !body?.requestId) { + return { + ok: false, + error: body?.error ?? `HTTP ${res.status}`, + details: body?.details, + statusCode: res.status, + }; + } + return body as RunInGameOperationStatus; } catch (err) { - const failure = studioServerOrpcFailure(err, "Run in Game failed"); - return { - ok: false, - error: failure.error, - details: failure.data?.details as RunInGameFailureDetails | undefined, - statusCode: failure.statusCode, - }; + return { ok: false, error: err instanceof Error ? err.message : "Run in Game failed" }; } } async function fetchRunInGameStatus(requestId: string): Promise { try { - const status = await studioServerClient.runInGame.status({ requestId }); - return status as RunInGameOperationStatus; + const res = await fetch(`/api/civ7/run-in-game/status?requestId=${encodeURIComponent(requestId)}`); + const body = (await res.json().catch(() => null)) as (Partial & { error?: string }) | null; + if (!res.ok || !body?.requestId) { + return { ok: false, error: body?.error ?? `HTTP ${res.status}`, statusCode: res.status }; + } + return body as RunInGameOperationStatus; } catch (err) { - const failure = studioServerOrpcFailure(err, "Run in Game status unavailable"); - return { ok: false, error: failure.error, statusCode: failure.statusCode }; + return { ok: false, error: err instanceof Error ? err.message : "Run in Game status unavailable" }; } } @@ -679,7 +677,28 @@ type AppContentProps = { }; function AppContent(props: AppContentProps) { - const { toast } = useToast(); + // Toast notifications now route through sonner. This adapter preserves the + // legacy `toast(message, { variant })` call shape used across this file so the + // migration is presentation-only: variant maps to the matching sonner method. + const toast = useCallback( + (message: string, options?: { variant?: "default" | "success" | "error" | "info"; duration?: number }) => { + const sonnerOptions = options?.duration !== undefined ? { duration: options.duration } : undefined; + switch (options?.variant) { + case "success": + sonnerToast.success(message, sonnerOptions); + break; + case "error": + sonnerToast.error(message, sonnerOptions); + break; + case "info": + sonnerToast.info(message, sonnerOptions); + break; + default: + sonnerToast(message, sonnerOptions); + } + }, + [], + ); const { themePreference, isLightMode, cyclePreference } = props; const theme = useMemo(() => createTheme(isLightMode), [isLightMode]); const initialAuthoringStateRef = useRef | undefined>(undefined); @@ -701,7 +720,6 @@ function AppContent(props: AppContentProps) { const [overlayVariantKeyPreference, setOverlayVariantKeyPreference] = useState(null); const [eraMode, setEraMode] = useState<"auto" | "fixed">("auto"); const [manualEra, setManualEra] = useState(1); - const [activeStudioView, setActiveStudioView] = useState("map"); const [recipeSectionCollapsed, setRecipeSectionCollapsed] = useState(false); const [configSectionCollapsed, setConfigSectionCollapsed] = useState(false); const [exploreStageExpanded, setExploreStageExpanded] = useState(true); @@ -722,13 +740,6 @@ function AppContent(props: AppContentProps) { preset: "none", seed: "123", }); - const [recipeDagState, setRecipeDagState] = useState({ - status: "idle", - recipeId: null, - dag: null, - error: null, - }); - const [expandedRecipeDagStageIds, setExpandedRecipeDagStageIds] = useState>(() => new Set()); const [setupConfig, setSetupConfig] = useState(() => initialAuthoringState?.setupConfig ?? DEFAULT_CIV7_STUDIO_SETUP_CONFIG ); @@ -751,47 +762,6 @@ function AppContent(props: AppContentProps) { ); const [runInGameOperation, setRunInGameOperation] = useState(null); - useEffect(() => { - if (activeStudioView !== "dag") return; - const recipeId = recipeSettings.recipe; - let cancelled = false; - setRecipeDagState((prev) => ({ - status: "loading", - recipeId, - dag: prev.recipeId === recipeId ? prev.dag : null, - error: null, - })); - void recipeDagClient.recipeDag.get({ recipeId }).then( - (dag: RecipeDagResult) => { - if (cancelled) return; - setRecipeDagState({ - status: "ready", - recipeId, - dag, - error: null, - }); - const firstStageId = dag.stages[0]?.stageId; - if (firstStageId) { - setExpandedRecipeDagStageIds((prev) => ( - prev.size > 0 ? prev : new Set([firstStageId]) - )); - } - }, - (err: unknown) => { - if (cancelled) return; - setRecipeDagState({ - status: "error", - recipeId, - dag: null, - error: formatErrorForUi(err), - }); - } - ); - return () => { - cancelled = true; - }; - }, [activeStudioView, recipeSettings.recipe]); - const overlaySuggestions = useMemo(() => getOverlaySuggestions(recipeSettings.recipe), [recipeSettings.recipe]); const overlaySelection = overlaySuggestions.find((opt) => opt.id === overlaySelectionId) ?? null; const overlayDataTypeKey = overlaySelection?.overlayDataTypeKey ?? null; @@ -1262,14 +1232,6 @@ function AppContent(props: AppContentProps) { const [selectedStageId, setSelectedStageId] = useState(""); const [selectedStepId, setSelectedStepId] = useState(""); - const handleRecipeDagStageToggle = useCallback((stageId: string) => { - setExpandedRecipeDagStageIds((prev) => { - const next = new Set(prev); - if (next.has(stageId)) next.delete(stageId); - else next.add(stageId); - return next; - }); - }, []); const recipeOptions = useMemo( () => STUDIO_RECIPE_OPTIONS.map((opt) => ({ value: opt.id, label: opt.label })), @@ -2316,10 +2278,6 @@ function AppContent(props: AppContentProps) { }, [runInGameOperation, toast]); const dataTypeModel = viz.dataTypeModel; - const riverLakeInspectorSummary = useMemo( - () => buildRiverLakeFloodplainInspectorSummary(viz.manifest), - [viz.manifest] - ); const dataTypeOptions: DataTypeOption[] = useMemo(() => { if (!dataTypeModel) return []; return dataTypeModel.dataTypes.map((dt) => ({ value: dt.dataTypeId, label: dt.label, group: dt.group })); @@ -2542,20 +2500,6 @@ function AppContent(props: AppContentProps) { [dataTypeModel, eraMode, manualEra, selectLayerFor] ); - const handleRiverLakeInspectorLayerSelect = useCallback( - (ref: RiverLakeInspectorLayerRef) => { - const stage = recipeArtifacts.uiMeta.stages.find((candidate) => - candidate.steps.some((step) => step.fullStepId === ref.stepId) - ); - if (stage) setSelectedStageId(stage.stageId); - setSelectedStepId(ref.stepId); - if (ref.visibility === "debug") viz.setShowDebugLayers(true); - viz.setSelectedStepId(ref.stepId); - viz.setSelectedLayerKey(ref.layerKey); - }, - [recipeArtifacts.uiMeta.stages, viz] - ); - const handleSpaceChange = useCallback( (next: string) => { if (!selection) return; @@ -2818,21 +2762,6 @@ function AppContent(props: AppContentProps) { return true; }, [showGrid, viz.effectiveLayer]); - const recipeDagView = ( - - ); - const canvas = (
@@ -2888,8 +2817,6 @@ function AppContent(props: AppContentProps) { const header = ( - ) : null} onHeaderHeightChange={handleHeaderHeightChange} /> ); @@ -3001,8 +2921,6 @@ function AppContent(props: AppContentProps) { if (!viz.activeBounds) return; deckApiRef.current?.fitToBounds(viz.activeBounds); }} - riverLakeInspectorSummary={riverLakeInspectorSummary} - onRiverLakeInspectorLayerSelect={handleRiverLakeInspectorLayerSelect} stageExpanded={exploreStageExpanded} onStageExpandedChange={setExploreStageExpanded} stepExpanded={exploreStepExpanded} @@ -3084,8 +3002,8 @@ function AppContent(props: AppContentProps) { ); return ( -
- {activeStudioView === "dag" ? recipeDagView : canvas} +
+ {canvas} {presetDialogs} - {activeStudioView === "map" ? ( - <> -
- {leftPanel} -
-
- {rightPanel} -
- - ) : null} +
+ {leftPanel} +
+
+ {rightPanel} +
{header} {footer} - {activeStudioView === "map" && error ? ( + {error ? (
{error} @@ -3125,8 +3039,9 @@ function AppContent(props: AppContentProps) { export function App() { const { preference, isLightMode, cyclePreference } = useThemePreference(); return ( - + - + + ); } diff --git a/apps/mapgen-studio/src/features/presets/PresetDialogs.tsx b/apps/mapgen-studio/src/features/presets/PresetDialogs.tsx index afffdea7df..91683ca350 100644 --- a/apps/mapgen-studio/src/features/presets/PresetDialogs.tsx +++ b/apps/mapgen-studio/src/features/presets/PresetDialogs.tsx @@ -1,50 +1,61 @@ import { useEffect, useState } from "react"; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, + Button, + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, Input, -} from "../../ui/components/ui"; +} from "../../components/ui"; + +/** + * Preset dialogs — error / save / confirm flows, migrated from the legacy + * hand-rolled AlertDialog to the token-driven shadcn Dialog. Open/confirm/cancel + * semantics are preserved: `onOpenChange` keeps the same open-state contract, + * Cancel/Close use a DialogClose, and the confirm action invokes the caller's + * callback. The `lightMode` prop is retained for call-site compatibility but no + * longer drives styling (the Dialog is token-driven via the `.dark` class). + */ export type PresetErrorDialogProps = Readonly<{ open: boolean; title: string; message: string; details?: ReadonlyArray; - lightMode: boolean; + lightMode?: boolean; onOpenChange: (open: boolean) => void; }>; export function PresetErrorDialog(props: PresetErrorDialogProps) { - const { open, title, message, details, lightMode, onOpenChange } = props; + const { open, title, message, details, onOpenChange } = props; return ( - - - - {title} - {message} - + + + + {title} + {message} + {details && details.length > 0 ? ( -
+          
             {details.join("\n")}
           
) : null} - - Close - - - + + + + + + +
); } export type PresetSaveDialogProps = Readonly<{ open: boolean; - lightMode: boolean; + lightMode?: boolean; initialLabel?: string; initialDescription?: string; onCancel: () => void; @@ -52,7 +63,7 @@ export type PresetSaveDialogProps = Readonly<{ }>; export function PresetSaveDialog(props: PresetSaveDialogProps) { - const { open, lightMode, initialLabel, initialDescription, onCancel, onConfirm } = props; + const { open, initialLabel, initialDescription, onCancel, onConfirm } = props; const [label, setLabel] = useState(initialLabel ?? ""); const [description, setDescription] = useState(initialDescription ?? ""); @@ -66,35 +77,35 @@ export function PresetSaveDialog(props: PresetSaveDialogProps) { const canSave = label.trim().length > 0; return ( - (!next ? onCancel() : undefined)} lightMode={lightMode}> - - - Save Config - Choose a name for this config. - -
+ (!next ? onCancel() : undefined)}> + + + Save Config + Choose a name for this config. + +
-
Label
+
Label
setLabel(e.target.value)} placeholder="Config name" />
-
Description (optional)
+
Description (optional)
setDescription(e.target.value)} placeholder="Short description" />
- - Cancel - + + + + + +
+
); } - export type PresetConfirmDialogProps = Readonly<{ open: boolean; - lightMode: boolean; + lightMode?: boolean; title: string; message: string; confirmLabel: string; @@ -121,19 +131,21 @@ export type PresetConfirmDialogProps = Readonly<{ }>; export function PresetConfirmDialog(props: PresetConfirmDialogProps) { - const { open, lightMode, title, message, confirmLabel, onCancel, onConfirm } = props; + const { open, title, message, confirmLabel, onCancel, onConfirm } = props; return ( - (!next ? onCancel() : undefined)} lightMode={lightMode}> - - - {title} - {message} - - - Cancel - {confirmLabel} - - - + (!next ? onCancel() : undefined)}> + + + {title} + {message} + + + + + + + + + ); } diff --git a/apps/mapgen-studio/src/ui/components/AppBrand.tsx b/apps/mapgen-studio/src/ui/components/AppBrand.tsx index 3188c4fc96..ae02014045 100644 --- a/apps/mapgen-studio/src/ui/components/AppBrand.tsx +++ b/apps/mapgen-studio/src/ui/components/AppBrand.tsx @@ -1,25 +1,21 @@ import React, { useState } from 'react'; import { ExternalLink, Github, User } from 'lucide-react'; + +/** + * AppBrand — the identity pill in the header, with a hover info card. + * + * Reskinned onto the design tokens: the pill and its hover card float over the + * deck.gl map, so they ride the `popover` tier with `backdrop-blur`; the theme + * follows the single `.dark` class rather than the legacy `isLightMode` hex + * ternaries. The `isLightMode` prop is retained for call-site compatibility + * during the shell migration but no longer drives styling. + */ interface AppBrandProps { - isLightMode: boolean; + isLightMode?: boolean; } -export const AppBrand: React.FC = ({ isLightMode }) => { + +export const AppBrand: React.FC = () => { const [isHovered, setIsHovered] = useState(false); - // ============================================================================ - // Styles - // ============================================================================ - const containerClass = isLightMode ? - 'bg-white/90 border-gray-200' : - 'bg-[#16161d]/90 border-[#26262e]'; - const textClass = isLightMode ? 'text-[#1f2933]' : 'text-[#e2e2e9]'; - const mutedClass = isLightMode ? 'text-[#6b7280]' : 'text-[#6a6a7c]'; - const linkClass = isLightMode ? - 'text-[#4b5563] hover:text-[#1f2933]' : - 'text-[#7a7a8c] hover:text-[#e2e2e9]'; - const dividerClass = isLightMode ? 'border-gray-200' : 'border-[#26262e]'; - // ============================================================================ - // Render - // ============================================================================ return (
= ({ isLightMode }) => { onMouseLeave={() => setIsHovered(false)}> {/* Main Pill */} -
- - - +
+ MapGen Studio - v0.1 + v0.1
{/* Hover Dropdown */} {isHovered && -
- +
{/* Description */} -

+

Procedural map generation toolkit for game developers.

-
+
{/* Links */} -
+
{/* Footer */} -

© 2024 • MIT License

+

© 2024 • MIT License

}
); -}; \ No newline at end of file +}; diff --git a/apps/mapgen-studio/src/ui/components/AppFooter.tsx b/apps/mapgen-studio/src/ui/components/AppFooter.tsx index 170367ab4a..760fd36b70 100644 --- a/apps/mapgen-studio/src/ui/components/AppFooter.tsx +++ b/apps/mapgen-studio/src/ui/components/AppFooter.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Bolt, Bot, Clipboard, Clock, Dices, MonitorPlay, Play, Radio, RotateCw, Square } from 'lucide-react'; -import { Button, Input } from './ui'; +import { Button, Input, Tooltip, TooltipContent, TooltipTrigger } from '../../components/ui'; import { MAP_SIZE_SHORT, LAYOUT } from '../constants'; import { formatResourceMode } from '../utils'; import type { RecipeSettings, WorldSettings, GenerationStatus } from '../types'; @@ -98,7 +98,6 @@ export const AppFooter: React.FC = ({ runInGameStatus, runInGameCurrentRelation = "unknown", isDirty, - lightMode, liveRuntime, liveGameStudioRelation = "unknown", onSyncFromLiveGame, @@ -108,20 +107,25 @@ export const AppFooter: React.FC = ({ autoRunEnabled, onAutoRunEnabledChange }) => { - const panelBg = lightMode ? 'bg-white/95' : 'bg-[#141418]/95'; - const panelBorder = lightMode ? 'border-gray-200' : 'border-[#2a2a32]'; - const textPrimary = lightMode ? 'text-[#1f2937]' : 'text-[#e8e8ed]'; - const textSecondary = lightMode ? 'text-[#6b7280]' : 'text-[#8a8a96]'; - const textMuted = lightMode ? 'text-[#9ca3af]' : 'text-[#5a5a66]'; - const dividerColor = lightMode ? 'bg-gray-200' : 'bg-[#2a2a32]'; + // Token-driven chrome; theme follows the single `.dark` class. The footer + // docks float over the deck.gl map, so they ride the `popover` tier. + const panelBg = 'bg-popover/95'; + const panelBorder = 'border-border'; + const textPrimary = 'text-foreground'; + const textSecondary = 'text-muted-foreground'; + const textMuted = 'text-muted-foreground/70'; + const dividerColor = 'bg-border'; + // Status dots report data the instrument observes (the one place real color + // belongs in the chrome): running = amber, error = destructive, ready = + // success. `isDirty` is chrome identity, so it uses the slate accent. const statusDotClass = status === 'running' ? - 'bg-amber-400' : + 'bg-warning' : status === 'error' ? - 'bg-red-400' : + 'bg-destructive' : isDirty ? - 'bg-orange-400' : - 'bg-emerald-400'; + 'bg-primary' : + 'bg-success'; const statusText = status === 'running' ? 'Running' : @@ -134,7 +138,7 @@ export const AppFooter: React.FC = ({ MAP_SIZE_SHORT[lastGlobalSettings.mapSize] || lastGlobalSettings.mapSize; const displayResources = formatResourceMode(lastGlobalSettings.resources); const liveDotClass = - liveRuntime?.status === "ok" ? "bg-emerald-400" : liveRuntime?.status === "error" ? "bg-red-400" : "bg-gray-400"; + liveRuntime?.status === "ok" ? "bg-success" : liveRuntime?.status === "error" ? "bg-destructive" : "bg-muted-foreground"; const liveText = liveRuntime?.status === "ok" ? liveRuntime.turn !== undefined || liveRuntime.seed !== undefined @@ -154,14 +158,14 @@ export const AppFooter: React.FC = ({ : null; const runInGameDotClass = runInGameCurrentRelation === "stale" - ? "bg-orange-400" + ? "bg-warning" : runInGameStatus?.status === "complete" - ? "bg-emerald-400" + ? "bg-success" : runInGameStatus?.status === "failed" || runInGameStatus?.status === "blocked" || runInGameStatus?.status === "uncertain" - ? "bg-red-400" + ? "bg-destructive" : isRunInGameRunning - ? "bg-amber-400" - : "bg-gray-400"; + ? "bg-warning" + : "bg-muted-foreground"; const runInGameButtonText = runInGamePrimaryActionLabel(runInGameStatus, runInGameCurrentRelation); const operationControlsDisabled = isRunning || isRunInGameRunning || isSaveDeployRunning; const liveSyncAvailable = @@ -249,14 +253,18 @@ export const AppFooter: React.FC = ({
{/* Last run info */} -
- + {lastRunSettings.seed} + + + Click to copy seed + · {displaySize} · @@ -266,44 +274,52 @@ export const AppFooter: React.FC = ({
- {/* Live Civ7 Panel */} + {/* Live Civ7 Panel. The "stale vs live game" emphasis is a warning about + data, so it uses the `warning` token (not the slate identity accent). */}
+ className={`h-10 inline-flex min-w-0 max-w-[420px] items-center gap-2 px-3 rounded-lg border backdrop-blur-sm ${panelBg} ${panelBorder}`}> - - + + {liveSyncTitle} + + + + + {liveRuntime?.autoplayActive ? : } + {autoplayButtonText} + + + {autoplayTitle} +
{/* Run Controls Panel */} @@ -316,107 +332,141 @@ export const AppFooter: React.FC = ({ Seed - updateSetting('seed', e.target.value)} - placeholder="Seed" - inputMode="numeric" - pattern="[0-9]*" - min={CIV7_STUDIO_SEED_MIN} - max={CIV7_STUDIO_SEED_MAX} - title={`Generation seed (${CIV7_STUDIO_SEED_MIN}-${CIV7_STUDIO_SEED_MAX})`} - disabled={operationControlsDisabled} - lightMode={lightMode} - className="w-20 font-mono" /> - + + + updateSetting('seed', e.target.value)} + placeholder="Seed" + inputMode="numeric" + pattern="[0-9]*" + min={CIV7_STUDIO_SEED_MIN} + max={CIV7_STUDIO_SEED_MAX} + aria-label={`Generation seed (${CIV7_STUDIO_SEED_MIN}-${CIV7_STUDIO_SEED_MAX})`} + disabled={operationControlsDisabled} + className="w-20 font-mono" /> + + {`Generation seed (${CIV7_STUDIO_SEED_MIN}-${CIV7_STUDIO_SEED_MAX})`} + {/* Reroll button */} - + + + + Re-roll: New seed and run + {/* Auto-run toggle */} - + + + + Auto-run: run current seed on config changes + {/* Run in Game button */} {saveDeployStatus && saveDeployStatus.status !== "complete" ? ( -
+ + +
-
- {saveDeployLabel} -
+
+ {saveDeployLabel} +
+ + {saveDeployTitle} + ) : null} {runInGameStatus ? ( -
+ + +
-
- {runInGamePhaseLabel} - {runInGameStateLabel ? ( - - {runInGameStateLabel} - - ) : null} -
+
+ {runInGamePhaseLabel} + {runInGameStateLabel ? ( + + {runInGameStateLabel} + + ) : null} +
+ + {runInGameTitle} + ) : null} {runInGameStatus && onRunInGameRetryStatus && runInGameCanRetryStatus(runInGameStatus) ? ( - + + + + Refresh Run in Game status + ) : null} {runInGameStatus && onCopyRunInGameDiagnostics ? ( - + + + + Copy Run in Game diagnostics + ) : null} - + + {runInGameButtonText} + + + {runInGameTitle} + - {/* Run button */} + {/* Run button — the one filled action; dirty emphasis is the slate accent */}
- {statsAccessory ? statsAccessory : null} - {setupOpen ? (
@@ -289,30 +277,7 @@ export const AppHeader: React.FC = ({
{/* Right: View Controls */} -
-
- - -
+
void; /** Callback when fit view is requested */ onFitView: () => void; - /** River/lake/floodplain proof navigator */ - riverLakeInspectorSummary?: RiverLakeFloodplainInspectorSummary | null; - /** Callback when a proof layer should be selected */ - onRiverLakeInspectorLayerSelect?: (layerRef: RiverLakeInspectorLayerRef) => void; /** Whether the stage section is expanded (optional controlled mode) */ stageExpanded?: boolean; /** Callback when stageExpanded changes (optional controlled mode) */ @@ -160,14 +151,11 @@ export const ExplorePanel: React.FC = ({ eraMax, onEraModeChange, onEraValueChange, - lightMode, showEdges, onShowEdgesChange, showDebugLayers, onShowDebugLayersChange, onFitView, - riverLakeInspectorSummary = null, - onRiverLakeInspectorLayerSelect, stageExpanded: stageExpandedProp, onStageExpandedChange, stepExpanded: stepExpandedProp, @@ -234,40 +222,35 @@ export const ExplorePanel: React.FC = ({ // ========================================================================== // Styles // ========================================================================== - const panelBg = lightMode ? 'bg-white/95' : 'bg-[#141418]/95'; - const panelBorder = lightMode ? 'border-gray-200' : 'border-[#2a2a32]'; - const textPrimary = lightMode ? 'text-[#1f2937]' : 'text-[#e8e8ed]'; - const textSecondary = lightMode ? 'text-[#6b7280]' : 'text-[#8a8a96]'; - const textMuted = lightMode ? 'text-[#9ca3af]' : 'text-[#5a5a66]'; - const borderSubtle = lightMode ? 'border-gray-100' : 'border-[#222228]'; - const hoverBg = lightMode ? 'hover:bg-gray-50' : 'hover:bg-[#1a1a1f]'; - const chipBg = lightMode ? 'bg-gray-100 text-gray-600' : 'bg-[#222228] text-[#8a8a96]'; + // Token-driven chrome; theme follows the single `.dark` class. The dock + // floats over the deck.gl map, so it rides the `popover` tier. Active list + // items use the steel contour (a thin rule + `bg-muted`), not a saturated + // slab. + const panelBg = 'bg-popover/95'; + const panelBorder = 'border-border'; + const textPrimary = 'text-foreground'; + const textSecondary = 'text-muted-foreground'; + const textMuted = 'text-muted-foreground/70'; + const borderSubtle = 'border-border-subtle'; + const hoverBg = 'hover:bg-accent'; const listMaxHeight = "max-h-[200px]"; // Stage list styles - const stageItemBase = `w-full text-left px-3 py-2 text-[11px] font-medium transition-colors cursor-pointer flex items-center gap-2`; - const stageItemActive = lightMode ? - 'bg-gray-100 text-[#1f2937]' : - 'bg-[#1a1a1f] text-[#e8e8ed]'; - const stageItemInactive = lightMode ? - 'text-[#6b7280] hover:bg-gray-50 hover:text-[#1f2937]' : - 'text-[#8a8a96] hover:bg-[#1a1a1f] hover:text-[#e8e8ed]'; + const stageItemBase = `w-full text-left px-3 py-2 text-data font-medium transition-colors cursor-pointer flex items-center gap-2`; + const stageItemActive = 'bg-accent text-foreground'; + const stageItemInactive = 'text-muted-foreground hover:bg-accent hover:text-foreground'; // Step/DataType list styles - const stepItemBase = `w-full text-left px-3 py-1.5 text-[11px] font-mono transition-colors cursor-pointer flex items-center gap-2 border-l-2`; - const stepItemActive = lightMode ? - 'border-gray-800 bg-gray-50 text-[#1f2937]' : - 'border-[#e8e8ed] bg-[#1a1a1f] text-[#e8e8ed]'; - const stepItemInactive = lightMode ? - 'border-transparent text-[#6b7280] hover:bg-gray-50 hover:text-[#1f2937]' : - 'border-transparent text-[#8a8a96] hover:bg-[#1a1a1f] hover:text-[#e8e8ed]'; - const iconBtn = `h-7 w-7 flex items-center justify-center rounded transition-colors shrink-0 ${lightMode ? 'text-[#6b7280] hover:text-[#1f2937] hover:bg-gray-100' : 'text-[#8a8a96] hover:text-[#e8e8ed] hover:bg-[#1a1a1f]'}`; - const iconBtnActive = `h-7 w-7 flex items-center justify-center rounded transition-colors shrink-0 ${lightMode ? 'text-[#1f2937] bg-gray-200' : 'text-[#e8e8ed] bg-[#222228]'}`; + const stepItemBase = `w-full text-left px-3 py-1.5 text-data font-mono transition-colors cursor-pointer flex items-center gap-2 border-l-2`; + const stepItemActive = 'border-primary bg-accent text-foreground'; + const stepItemInactive = 'border-transparent text-muted-foreground hover:bg-accent hover:text-foreground'; + const iconBtn = 'h-7 w-7 flex items-center justify-center rounded transition-colors shrink-0 text-muted-foreground hover:text-foreground hover:bg-accent'; + const iconBtnActive = 'h-7 w-7 flex items-center justify-center rounded transition-colors shrink-0 text-foreground bg-muted'; const stageBadge = (isActive: boolean) => ` - w-5 h-5 flex items-center justify-center rounded-full text-[10px] font-semibold shrink-0 - ${isActive ? lightMode ? 'bg-gray-300 text-gray-700' : 'bg-[#3a3a44] text-[#e8e8ed]' : lightMode ? 'bg-gray-100 text-gray-400' : 'bg-[#222228] text-[#5a5a66]'} + w-5 h-5 flex items-center justify-center rounded-full text-label font-semibold shrink-0 + ${isActive ? 'bg-muted text-foreground' : 'bg-muted/50 text-muted-foreground'} `; const stepBadge = (isActive: boolean) => ` w-4 h-4 flex items-center justify-center rounded text-[9px] font-mono shrink-0 - ${isActive ? lightMode ? 'bg-gray-200 text-gray-800' : 'bg-[#3a3a44] text-[#e8e8ed]' : lightMode ? 'bg-gray-100 text-gray-400' : 'bg-[#222228] text-[#5a5a66]'} + ${isActive ? 'bg-muted text-foreground' : 'bg-muted/50 text-muted-foreground'} `; // Render mode icons map const getRenderModeIcon = (value: string) => { @@ -330,67 +313,6 @@ export const ExplorePanel: React.FC = ({ return { ...prev, [key]: !current }; }); }; - - const inspectorRows = riverLakeInspectorSummary?.rows ?? []; - const statusChipClass = (status: RiverLakeInspectorClaimStatus) => { - if (status === "pass") { - return lightMode ? "bg-emerald-50 text-emerald-700" : "bg-emerald-950/60 text-emerald-300"; - } - if (status === "available") { - return lightMode ? "bg-sky-50 text-sky-700" : "bg-sky-950/60 text-sky-300"; - } - if (status === "fail") { - return lightMode ? "bg-red-50 text-red-700" : "bg-red-950/60 text-red-300"; - } - if (status === "out-of-scope") { - return lightMode ? "bg-gray-100 text-gray-500" : "bg-[#222228] text-[#7a7a86]"; - } - return lightMode ? "bg-amber-50 text-amber-700" : "bg-amber-950/60 text-amber-300"; - }; - const statusLabel = (status: RiverLakeInspectorClaimStatus) => { - switch (status) { - case "pass": - return "ready"; - case "available": - return "inspect"; - case "fail": - return "fail"; - case "out-of-scope": - return "skip"; - case "unresolved": - default: - return "open"; - } - }; - const formatCountLabel = (key: string) => { - switch (key) { - case "layers": - return "layers"; - case "default": - return "shown"; - case "debug": - return "debug"; - default: - return key; - } - }; - const formatLayerButtonLabel = (ref: RiverLakeInspectorLayerRef) => { - if (ref.dataTypeKey.includes("projectedRiverMask")) return "projected"; - if (ref.dataTypeKey.includes("plannedMinorRiverMask")) return "minor"; - if (ref.dataTypeKey.includes("plannedMajorRiverMask")) return "major"; - if (ref.dataTypeKey.includes("engineRiverMask")) return "terrain"; - if (ref.dataTypeKey.includes("Metadata")) return "metadata"; - if (ref.dataTypeKey.includes("engineMinorRiverMask")) return "minor meta"; - if (ref.dataTypeKey.includes("riverMismatchMask")) return "mismatch"; - if (ref.dataTypeKey.includes("lakePlan")) return "lake plan"; - if (ref.dataTypeKey.includes("plannedLakeMask")) return "planned"; - if (ref.dataTypeKey.includes("engineLakeMask")) return "engine"; - if (ref.dataTypeKey.includes("rejectedLakeMask")) return "rejected"; - if (ref.dataTypeKey.includes("featureType")) return "features"; - if (ref.dataTypeKey.includes("rejectionMask")) return "rejects"; - const parts = ref.dataTypeKey.split("."); - return parts[parts.length - 1] ?? ref.dataTypeKey; - }; // ========================================================================== // Render // ========================================================================== @@ -489,63 +411,6 @@ export const ExplorePanel: React.FC = ({
) : null} - {/* WATER PROOF SECTION */} - {inspectorRows.length > 0 ? ( - <> -
-
-
- - - Water Proof - -
- {inspectorRows.length} -
-
-
- {inspectorRows.map((row) => ( -
-
-
-
{row.laneLabel}
-
- {row.label} -
-
- - {statusLabel(row.claimStatus)} - -
-
- {Object.entries(row.counts).map(([key, value]) => ( - - {formatCountLabel(key)} {value} - - ))} - {row.layerRefs.slice(0, 4).map((ref) => ( - - ))} -
-
- ))} -
- - ) : null} - {/* 3. LAYERS SECTION */}
- + + Fit to view + + + + + + + + {showEdges ? 'Hide edges' : 'Show edges'} +
{/* Right: Render */}
- Render + Render
{renderModeOptions.map((option) => ( - + + + + + {option.label} + ))}
@@ -653,50 +532,59 @@ export const ExplorePanel: React.FC = ({
{/* Left: Space */}
- Space + Space
{spaceOptions.map((option) => ( - + + + + + {option.label} + ))}
{/* Right: Debug toggle */} - + + + + + {showDebugLayers ? "Hide debug layers" : "Show debug layers"} +
{eraEnabled ? (
- Era - + Era + + + + + {eraMode === "auto" ? "Auto (follow selected layer)" : "Manual era"} +
= ({ value={eraValue} disabled={eraMode === "auto"} onChange={(e) => onEraValueChange(Number(e.target.value))} - className="w-full accent-[#64748b]" + className="w-full accent-primary" /> -
+
{`Era ${eraValue}`} {`${eraMin}-${eraMax}`}
@@ -717,35 +605,35 @@ export const ExplorePanel: React.FC = ({ {variantOptions.length > 1 ? ( ) : null} {overlayOptions.length ? (
- + + + + + {showAllSteps ? 'Focus Current Step' : 'Show All Steps'} +
@@ -352,26 +362,34 @@ export const RecipePanel: React.FC = ({
- - - + + + + + Reset to Defaults + + + + + + + {showJson ? 'Show Form View' : 'Show JSON View'} +
{/* Config Form / JSON */} @@ -379,11 +397,10 @@ export const RecipePanel: React.FC = ({ className={`px-3 pb-3 ${overridesDisabled ? 'opacity-40 pointer-events-none select-none' : ''}`}> {showJson ? -
+
+                className={`text-label font-mono leading-relaxed ${textMuted} whitespace-pre-wrap break-all`}>
 
                     {JSON.stringify(filteredConfig, null, 2)}
                   
@@ -412,29 +429,33 @@ export const RecipePanel: React.FC = ({
- + + + + + {saveTitle} + {showSaveMenu && <> @@ -494,7 +515,7 @@ export const RecipePanel: React.FC = ({ onDeletePreset(); setShowSaveMenu(false); }} - className={`w-full text-left px-3 py-2 text-[11px] text-red-600 ${hoverBg} rounded-b-lg border-t ${borderSubtle}`}> + className={`w-full text-left px-3 py-2 text-data text-destructive ${hoverBg} rounded-b-lg border-t ${borderSubtle}`}> Delete Scratch @@ -513,31 +534,33 @@ export const RecipePanel: React.FC = ({
{/* Reset Confirmation Dialog */} - - - - - }> + + + + + Reset Config - - + + This will reset all config overrides to their default values. - - - - Cancel - + + + + + + + + + + ); }; diff --git a/apps/mapgen-studio/src/ui/components/ViewControls.tsx b/apps/mapgen-studio/src/ui/components/ViewControls.tsx index 805fcb288b..5ec3636302 100644 --- a/apps/mapgen-studio/src/ui/components/ViewControls.tsx +++ b/apps/mapgen-studio/src/ui/components/ViewControls.tsx @@ -3,9 +3,15 @@ import React from 'react'; // VIEW CONTROLS // ============================================================================ // Toolbar for theme toggle and grid visibility. +// +// Reskinned onto the design tokens: the dock floats over the map on the +// `popover` tier; icon buttons rest on `text-muted-foreground` and lift to +// `bg-accent` on hover / `bg-muted` when active. Native `title=` hints are now +// the shadcn Tooltip (token-styled, delay-grouped under the shell provider). // ============================================================================ -import { Grid3X3, Sun, Moon, Monitor } from 'lucide-react'; +import { Sun, Moon, Monitor } from 'lucide-react'; import { cn } from '../utils'; +import { Tooltip, TooltipContent, TooltipTrigger } from '../../components/ui'; import type { ThemePreference } from '../types'; // ============================================================================ // Props @@ -15,8 +21,8 @@ export interface ViewControlsProps { themePreference: ThemePreference; /** Callback to cycle theme preference */ onThemeCycle: () => void; - /** Light mode flag for styling */ - isLightMode: boolean; + /** Light mode flag (retained for call-site compatibility; styling is token-driven) */ + isLightMode?: boolean; /** Whether grid is visible */ showGrid: boolean; /** Callback when grid visibility changes */ @@ -46,65 +52,67 @@ const THEME_CONFIG: Record< } }; // ============================================================================ +// Styles (token-driven; theme follows the `.dark` class) +// ============================================================================ +const iconBtn = cn( + 'h-7 w-7 flex items-center justify-center rounded transition-colors', + 'text-muted-foreground hover:bg-accent hover:text-foreground' +); +const iconBtnActive = cn( + 'h-7 w-7 flex items-center justify-center rounded transition-colors', + 'bg-muted text-foreground' +); +// ============================================================================ // Component // ============================================================================ export const ViewControls: React.FC = ({ themePreference, onThemeCycle, - isLightMode, showGrid, onShowGridChange }) => { - // ========================================================================== - // Styles - // ========================================================================== - const panelBg = isLightMode ? 'bg-white/70' : 'bg-[#141418]/62'; - const panelBorder = isLightMode ? 'border-white/80' : 'border-white/10'; - const dividerColor = isLightMode ? 'bg-gray-200' : 'bg-[#2a2a32]'; - // Icon button styles based on theme - const iconBtn = cn( - 'h-7 w-7 flex items-center justify-center rounded transition-colors', - isLightMode ? - 'text-[#6b7280] hover:bg-gray-100 hover:text-[#1f2937]' : - 'text-[#8a8a96] hover:bg-[#1a1a1f] hover:text-[#e8e8ed]' - ); - const iconBtnActive = cn( - 'h-7 w-7 flex items-center justify-center rounded transition-colors', - isLightMode ? 'bg-gray-200 text-[#1f2937]' : 'bg-[#222228] text-[#e8e8ed]' - ); const { icon: ThemeIcon, tooltip: themeTooltip } = THEME_CONFIG[themePreference]; + const gridTooltip = showGrid ? 'Hide grid' : 'Show grid'; // ========================================================================== // Render // ========================================================================== return (
{/* Theme toggle */} - + + + + {themeTooltip} + -