diff --git a/apps/mapgen-studio/.interface-design/system.md b/apps/mapgen-studio/.interface-design/system.md index 3fb8ff3873..dc24a3cda7 100644 --- a/apps/mapgen-studio/.interface-design/system.md +++ b/apps/mapgen-studio/.interface-design/system.md @@ -219,3 +219,23 @@ Decisions (see `docs/projects/mapgen-studio-redesign/pass-4-design-fixes.md`): Reset/Show-JSON); collapsed by default, manual expand; optional sticky auto-expand-on-scroll toggle in the config toolbar, default OFF; identical in focused and unfocused modes. + +## Pass-5 amendment (2026-06-11, user-grounded): zoning v2 — Game bar / World console + +Decisions (see `docs/projects/mapgen-studio-redesign/pass-5-design-fixes.md`); +supersedes the Pass-4 dock placement, keeps the console split + icon contract: + +- **Top bar = THE Game toolbar.** One bar (identity `Gamepad2` + "Game") + unifies the saved-config selector, the GameConsole command cluster (inline, + no panel chrome or "Civ7" label of its own), and a trailing **icon-only** + game-setup disclosure (`SlidersHorizontal` + chevron — the dropdown row is + the label) expanding pure game setup: Leader · Civ · Difficulty · Speed. + **No map/world settings in the top bar, ever.** +- **Bottom bar = THE World/Map console** (identity `Globe` + "World"): studio + status · History · Size · Players · Resources · Seed · reroll · auto-run · + Run. Map settings author here; the relocated selects ride the shared + operation gate. Resources is a map setting (`WorldSettings`), not game setup. +- **History affordance:** the last-run cluster compresses into one icon button + (`History`); hover tooltip presents the run (seed/size/players/resources), + the accessible name mirrors it, click copies the last seed (the affordance + the old inline seed button carried). Tooltip, never a popup. diff --git a/apps/mapgen-studio/src/app/StudioShell.tsx b/apps/mapgen-studio/src/app/StudioShell.tsx index e3f6f96c62..1d02f98e87 100644 --- a/apps/mapgen-studio/src/app/StudioShell.tsx +++ b/apps/mapgen-studio/src/app/StudioShell.tsx @@ -2200,8 +2200,6 @@ export function StudioShell(props: StudioShellProps) { onThemeCycle={cyclePreference} showGrid={showGrid} onShowGridChange={setShowGrid} - globalSettings={worldSettings} - onGlobalSettingsChange={setWorldSettings} setupConfig={setupConfig} setupOptions={setupControlOptions} onSetupConfigChange={setSetupConfig} @@ -2328,6 +2326,8 @@ export function StudioShell(props: StudioShellProps) { status={status} lastRunSettings={lastRunSettings} lastGlobalSettings={lastGlobalSettings} + globalSettings={worldSettings} + onGlobalSettingsChange={setWorldSettings} currentSettings={recipeSettings} onSettingsChange={setRecipeSettings} onRun={triggerRun} diff --git a/apps/mapgen-studio/src/ui/components/AppFooter.tsx b/apps/mapgen-studio/src/ui/components/AppFooter.tsx index d8bccc5cd9..7e96d8b327 100644 --- a/apps/mapgen-studio/src/ui/components/AppFooter.tsx +++ b/apps/mapgen-studio/src/ui/components/AppFooter.tsx @@ -1,20 +1,22 @@ import React from 'react'; -import { Bolt, Clock, Dices, Play } from 'lucide-react'; +import { Bolt, Dices, Globe, History, Play } from 'lucide-react'; import { Button, Input, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../components/ui'; -import { MAP_SIZE_SHORT, LAYOUT } from '../constants'; +import { OptionSelect } from './OptionSelect'; +import { MAP_SIZE_OPTIONS, MAP_SIZE_SHORT, PLAYER_COUNT_OPTIONS, RESOURCE_MODE_OPTIONS, LAYOUT } from '../constants'; import { formatResourceMode } from '../utils'; import type { RecipeSettings, WorldSettings, GenerationStatus } from '../types'; import { CIV7_STUDIO_SEED_MAX, CIV7_STUDIO_SEED_MIN } from '../../features/civ7Setup/seedPolicy'; // ============================================================================ -// APP FOOTER — the studio console (Pass-4 game-console-dock spec) +// APP FOOTER — the World/Map console (Pass-5 toolbar-architecture-v2 spec) // ============================================================================ -// The footer is the STUDIO zone: one centered console carrying studio-runtime -// status and run controls (status · last run · seed · reroll · auto-run · -// Run). Everything that observes or commands live Civ7 lives in `GameConsole`, -// docked beneath the world bar in the header (top = game, bottom = studio). -// The footer still receives the game-side in-flight booleans because seed/ -// reroll/run share one operation gate across both zones (behavior parity). +// The footer is the WORLD/MAP zone: one centered console authoring the map +// (size · players · resources · seed) and driving the studio iteration loop +// (status · History · reroll · auto-run · Run). Everything that observes or +// commands live Civ7 lives in `GameConsole`, composed into the header's Game +// bar (top = game, bottom = world/map). The footer still receives the +// game-side in-flight booleans because its authoring/run controls share one +// operation gate across both zones (behavior parity). // ============================================================================ export interface AppFooterProps { @@ -24,6 +26,10 @@ export interface AppFooterProps { lastRunSettings: RecipeSettings; /** World settings from the last completed run */ lastGlobalSettings: WorldSettings; + /** Current world settings (size, players, resources) */ + globalSettings: WorldSettings; + /** Callback when world settings change */ + onGlobalSettingsChange: (settings: WorldSettings) => void; /** Current recipe settings (for seed input) */ currentSettings: RecipeSettings; /** Callback when recipe settings change */ @@ -52,6 +58,8 @@ export const AppFooter: React.FC = ({ status, lastRunSettings, lastGlobalSettings, + globalSettings, + onGlobalSettingsChange, currentSettings, onSettingsChange, onRun, @@ -104,6 +112,20 @@ export const AppFooter: React.FC = ({ [key]: value }); }; + const updateWorldSetting = ( + key: K, + value: WorldSettings[K]) => + { + onGlobalSettingsChange({ + ...globalSettings, + [key]: value + }); + }; + // The History affordance compresses the old inline last-run cluster: the + // tooltip presents the run, the accessible name mirrors it (a11y + static + // markup parity), and the click keeps the copy-seed behavior the inline + // seed button used to carry. + const historyLabel = `Run history — last run: seed ${lastRunSettings.seed}, ${displaySize}, ${lastGlobalSettings.playerCount} players, ${displayResources} resources. Click to copy seed.`; const handleCopySeed = async () => { try { await navigator.clipboard.writeText(lastRunSettings.seed); @@ -126,11 +148,21 @@ export const AppFooter: React.FC = ({ height: FOOTER_HEIGHT }}> - {/* Studio console — studio-runtime status + run controls, centered. */} + {/* World/Map console — map authoring + studio iteration loop, centered. */}
+ {/* Console identity: the map/world zone, mirroring the Game bar. */} +
+ + + World + +
+ +
+ {/* Status indicator */}
@@ -139,37 +171,88 @@ export const AppFooter: React.FC = ({
+ {/* Run history — the collapsed last-run cluster. */} + + + + + +
+ + Last run + + {lastRunSettings.seed} + + {displaySize} · {lastGlobalSettings.playerCount}p · {displayResources} + + Click to copy seed +
+
+
+
- {/* Last Run label */} -
- - + {/* Map settings (Pass-5: no map settings in the top bar) */} +
+ + Size + + + updateWorldSetting('mapSize', value as WorldSettings['mapSize']) + } + options={MAP_SIZE_OPTIONS.map((opt) => ({ + value: opt.value, + label: opt.label + }))} + ariaLabel="World size" + disabled={operationControlsDisabled} + className="w-24" /> +
- Last +
+ + Players + + updateWorldSetting('playerCount', parseInt(value, 10)) + } + options={PLAYER_COUNT_OPTIONS.map((count) => ({ + value: count.toString(), + label: count.toString() + }))} + ariaLabel="Players" + disabled={operationControlsDisabled} + className="w-14" />
- {/* Last run info */} -
- - - - - Click to copy seed - - · - {displaySize} - · - {lastGlobalSettings.playerCount}p - · - {displayResources} +
+ + Resources + + + updateWorldSetting('resources', value as WorldSettings['resources']) + } + options={RESOURCE_MODE_OPTIONS.map((opt) => ({ + value: opt.value, + label: opt.label + }))} + ariaLabel="Resources" + disabled={operationControlsDisabled} + className="w-24" />
diff --git a/apps/mapgen-studio/src/ui/components/AppHeader.tsx b/apps/mapgen-studio/src/ui/components/AppHeader.tsx index e281bef743..ec2a9fc522 100644 --- a/apps/mapgen-studio/src/ui/components/AppHeader.tsx +++ b/apps/mapgen-studio/src/ui/components/AppHeader.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { ChevronDown, Globe, SlidersHorizontal } from 'lucide-react'; +import { ChevronDown, Gamepad2, SlidersHorizontal } from 'lucide-react'; import { AppBrand } from './AppBrand'; import { ViewControls } from './ViewControls'; import { OptionSelect } from './OptionSelect'; @@ -9,19 +9,12 @@ import { updateStudioSetupPlayerOption, type Civ7StudioSetupConfig } from '../../features/civ7Setup/setupConfig'; -import { - MAP_SIZE_OPTIONS, - PLAYER_COUNT_OPTIONS, - RESOURCE_MODE_OPTIONS } from -'../constants'; -import type { ThemePreference, WorldSettings } from '../types'; +import type { ThemePreference } from '../types'; export interface AppHeaderProps { themePreference: ThemePreference; onThemeCycle: () => void; showGrid: boolean; onShowGridChange: (show: boolean) => void; - globalSettings: WorldSettings; - onGlobalSettingsChange: (settings: WorldSettings) => void; setupConfig: Civ7StudioSetupConfig; setupOptions: { savedConfigOptions: ReadonlyArray<{ value: string; label: string }>; @@ -34,9 +27,9 @@ export interface AppHeaderProps { onSavedConfigChange: (configId: string) => void; onHeaderHeightChange?: (height: number) => void; /** - * The live-game console (Pass-4 vertical zoning: top = game, bottom = - * studio). Rendered as a centered row beneath the world bar — after the - * transient setup panel, so the disclosure stays attached to its button. + * The live-game command cluster (Pass-5 zoning v2: top = Game, bottom = + * World/Map). Composed INLINE into the Game bar row, between the + * saved-config selector and the trailing setup disclosure. */ gameConsole?: React.ReactNode; } @@ -45,8 +38,6 @@ export const AppHeader: React.FC = ({ onThemeCycle, showGrid, onShowGridChange, - globalSettings, - onGlobalSettingsChange, setupConfig, setupOptions, onSetupConfigChange, @@ -64,16 +55,7 @@ export const AppHeader: React.FC = ({ const textMuted = 'text-muted-foreground/70'; const dividerColor = 'bg-border'; const setupButtonClassName = - 'flex h-7 shrink-0 items-center gap-1.5 rounded border border-input bg-input-background px-2.5 text-data font-medium text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring'; - const updateSetting = ( - key: K, - value: WorldSettings[K]) => - { - onGlobalSettingsChange({ - ...globalSettings, - [key]: value - }); - }; + 'flex h-7 shrink-0 items-center gap-1.5 rounded border border-input bg-input-background px-2 text-data font-medium text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring'; const localPlayerSetup = getLocalPlayerSetup(setupConfig); const updateLeader = (value: string) => { onSetupConfigChange(updateStudioSetupPlayerOption(setupConfig, 'PlayerLeader', value || undefined)); @@ -113,58 +95,22 @@ export const AppHeader: React.FC = ({
- {/* Center: World Settings */} + {/* Center: the Game bar — config selector, live command cluster, and + the trailing game-setup disclosure. Map/world settings live in the + footer's World/Map console (Pass-5 zoning v2). */}
- + - World + Game
-
- - Size - - - updateSetting('mapSize', value as WorldSettings['mapSize']) - } - options={MAP_SIZE_OPTIONS.map((opt) => ({ - value: opt.value, - label: opt.label - }))} - ariaLabel="World size" - className="w-24" /> -
- -
- -
- - Players - - - updateSetting('playerCount', parseInt(value, 10)) - } - options={PLAYER_COUNT_OPTIONS.map((count) => ({ - value: count.toString(), - label: count.toString() - }))} - ariaLabel="Players" - className="w-14" /> -
- -
-
Config @@ -177,16 +123,26 @@ export const AppHeader: React.FC = ({ className="w-44 max-w-[34vw]" />
+ {gameConsole ? ( + <> +
+ {gameConsole} + + ) : null} +
+ {/* Game-setup disclosure: icon-only (the dropdown row IS the + label), pinned last in the bar per the Pass-5 spec. */}
@@ -196,25 +152,6 @@ export const AppHeader: React.FC = ({ id="app-header-setup-panel" className={`flex min-h-10 max-w-full flex-wrap items-center justify-center gap-x-3 gap-y-2 px-3 py-1.5 rounded-lg border backdrop-blur-sm ${panelBg} ${panelBorder}`}> -
- - Resources - - - updateSetting('resources', value as WorldSettings['resources']) - } - options={RESOURCE_MODE_OPTIONS.map((opt) => ({ - value: opt.value, - label: opt.label - }))} - ariaLabel="Resources" - className="w-24" /> -
- -
-
Leader @@ -270,14 +207,6 @@ export const AppHeader: React.FC = ({
) : null} - - {gameConsole ? ( - // Live-game console docked beneath the world bar (and beneath the - // open setup panel): the header zone is the GAME zone, the footer - // is the studio zone. Growth here reflows the side panels through - // the measured-header mechanism above. -
{gameConsole}
- ) : null}
{/* Right: View Controls */} diff --git a/apps/mapgen-studio/src/ui/components/GameConsole.tsx b/apps/mapgen-studio/src/ui/components/GameConsole.tsx index b0b3a266cf..efa540adb0 100644 --- a/apps/mapgen-studio/src/ui/components/GameConsole.tsx +++ b/apps/mapgen-studio/src/ui/components/GameConsole.tsx @@ -16,13 +16,16 @@ import { // ============================================================================ // GAME CONSOLE // ============================================================================ -// The footer's live-Civ7 area (Pass-3 footer-consoles spec): every control and -// readout that observes or commands the LIVE GAME lives here — live runtime -// chip + apply-suggestion bridge, autoplay, Run in Game with its status/retry/ -// diagnostics, and the save-deploy chip. New live-game controls belong in this -// unit, not in the studio console. The studio↔game bridge cues (stale warning -// ring, Current/Stale/Previous relation chip) are computed against the current -// authored Studio state and surface here, beside the controls they qualify. +// The Game bar's command cluster (Pass-5 toolbar-architecture-v2 spec): every +// control and readout that observes or commands the LIVE GAME lives here — +// live runtime chip + apply-suggestion bridge, autoplay, Run in Game with its +// status/retry/diagnostics, and the save-deploy chip. New live-game controls +// belong in this unit, not in the studio console. The cluster renders inline +// with NO panel chrome or identity label of its own: AppHeader composes it +// into the Game bar row, whose "Game" identity covers it. The studio↔game +// bridge cues (stale warning ring, Current/Stale/Previous relation chip) are +// computed against the current authored Studio state and surface here, beside +// the controls they qualify. // ============================================================================ /** Read-only live Civ7 runtime snapshot the console renders. */ @@ -87,9 +90,6 @@ export const GameConsole: React.FC = ({ onCopyRunInGameDiagnostics, saveDeployStatus, }) => { - // Token-driven chrome; the console floats over the map on the popover tier. - const panelBg = 'bg-popover/95'; - const panelBorder = 'border-border'; const textPrimary = 'text-foreground'; const textMuted = 'text-muted-foreground/70'; // The "stale vs live game" emphasis is a warning about data, so it uses the @@ -165,15 +165,7 @@ export const GameConsole: React.FC = ({ ].filter(Boolean).join("\n"); return ( -
- - {/* Console identity: this is the live-game area, named so it can grow. */} - - Civ7 - -
- +