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} + -