Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions apps/mapgen-studio/.interface-design/system.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 2 additions & 2 deletions apps/mapgen-studio/src/app/StudioShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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}
Expand Down
153 changes: 118 additions & 35 deletions apps/mapgen-studio/src/ui/components/AppFooter.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 */
Expand Down Expand Up @@ -52,6 +58,8 @@ export const AppFooter: React.FC<AppFooterProps> = ({
status,
lastRunSettings,
lastGlobalSettings,
globalSettings,
onGlobalSettingsChange,
currentSettings,
onSettingsChange,
onRun,
Expand Down Expand Up @@ -104,6 +112,20 @@ export const AppFooter: React.FC<AppFooterProps> = ({
[key]: value
});
};
const updateWorldSetting = <K extends keyof WorldSettings,>(
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);
Expand All @@ -126,11 +148,21 @@ export const AppFooter: React.FC<AppFooterProps> = ({
height: FOOTER_HEIGHT
}}>

{/* Studio console — studio-runtime status + run controls, centered. */}
{/* World/Map console — map authoring + studio iteration loop, centered. */}

<div
className={`h-10 shrink-0 inline-flex items-center gap-3 px-3 rounded-lg border backdrop-blur-sm ${panelBg} ${panelBorder}`}>

{/* Console identity: the map/world zone, mirroring the Game bar. */}
<div className="flex items-center gap-1.5">
<Globe className={`w-4 h-4 ${textMuted}`} />
<span className={`text-label font-semibold uppercase tracking-wider ${textSecondary}`}>
World
</span>
</div>

<div className={`w-px h-5 ${dividerColor}`} />

{/* Status indicator */}
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${statusDotClass}`} />
Expand All @@ -139,37 +171,88 @@ export const AppFooter: React.FC<AppFooterProps> = ({
</span>
</div>

{/* Run history — the collapsed last-run cluster. */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
onClick={handleCopySeed}
aria-label={historyLabel}
title={historyLabel}>

<History className="w-3.5 h-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="flex flex-col gap-0.5">
<span className="text-label font-medium uppercase tracking-wider opacity-70">
Last run
</span>
<span className="font-mono">{lastRunSettings.seed}</span>
<span>
{displaySize} · {lastGlobalSettings.playerCount}p · {displayResources}
</span>
<span className="opacity-70">Click to copy seed</span>
</div>
</TooltipContent>
</Tooltip>

<div className={`w-px h-5 ${dividerColor}`} />

{/* Last Run label */}
<div className="flex items-center gap-1.5">
<Clock className={`w-3.5 h-3.5 ${textMuted}`} />
<span
className={`text-label font-medium uppercase tracking-wider ${textSecondary}`}>
{/* Map settings (Pass-5: no map settings in the top bar) */}
<div className="flex items-center gap-2">
<span className={`text-label font-medium uppercase tracking-wider shrink-0 ${textMuted}`}>
Size
</span>
<OptionSelect
value={globalSettings.mapSize}
onValueChange={(value) =>
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" />
</div>

Last
<div className="flex items-center gap-2">
<span className={`text-label font-medium uppercase tracking-wider shrink-0 ${textMuted}`}>
Players
</span>
<OptionSelect
value={globalSettings.playerCount.toString()}
onValueChange={(value) =>
updateWorldSetting('playerCount', parseInt(value, 10))
}
options={PLAYER_COUNT_OPTIONS.map((count) => ({
value: count.toString(),
label: count.toString()
}))}
ariaLabel="Players"
disabled={operationControlsDisabled}
className="w-14" />
</div>

{/* Last run info */}
<div className="flex items-center gap-2 text-data">
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={handleCopySeed}
className={`font-mono ${textPrimary} hover:text-primary hover:underline transition-colors cursor-pointer`}>

{lastRunSettings.seed}
</button>
</TooltipTrigger>
<TooltipContent>Click to copy seed</TooltipContent>
</Tooltip>
<span className={textMuted}>·</span>
<span className={textPrimary}>{displaySize}</span>
<span className={textMuted}>·</span>
<span className={textPrimary}>{lastGlobalSettings.playerCount}p</span>
<span className={textMuted}>·</span>
<span className={textPrimary}>{displayResources}</span>
<div className="flex items-center gap-2">
<span className={`text-label font-medium uppercase tracking-wider shrink-0 ${textMuted}`}>
Resources
</span>
<OptionSelect
value={globalSettings.resources}
onValueChange={(value) =>
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" />
</div>

<div className={`w-px h-5 ${dividerColor}`} />
Expand Down
Loading