feat(ui): app shell with nav rail

This commit is contained in:
ginnoir
2026-06-11 21:24:42 -05:00
parent ed9607e9e8
commit ee79872d4d
8 changed files with 432 additions and 48 deletions
+28 -48
View File
@@ -1,65 +1,45 @@
import { useEffect } from 'react';
import { gameRuntime } from '../state/runtime';
import { useGameStore } from '../state/store';
import { ActionPanel } from './ActionPanel';
import { EventLog } from './EventLog';
import { ResourceBar } from './ResourceBar';
import { SettingsDrawer } from './SettingsDrawer';
import { StoryPanel } from './StoryPanel';
import { AboutPanel } from './AboutPanel';
import { AppShell } from './AppShell';
import { NavRail } from './NavRail';
import { PlayPanel } from './PlayPanel';
import { RightRail } from './RightRail';
import { SettingsPanel } from './SettingsPanel';
import { StoryView } from './StoryView';
/**
* M1 playable-loop shell. Boots the runtime once on mount; everything else
* renders from the Zustand store the runtime feeds.
*/
export function App() {
const storyHasUnread = useGameStore((s) => s.storyHasUnread);
const settingsOpen = useGameStore((s) => s.settingsOpen);
const setSettingsOpen = useGameStore((s) => s.setSettingsOpen);
const activePanel = useGameStore((s) => s.activePanel);
useEffect(() => {
void gameRuntime.boot();
}, []);
const renderActivePanel = () => {
switch (activePanel) {
case 'play':
return <PlayPanel />;
case 'story':
return <StoryView />;
case 'settings':
return <SettingsPanel />;
case 'about':
return <AboutPanel />;
default:
return <PlayPanel />;
}
};
return (
<>
<StoryPanel />
<main className="mx-auto flex min-h-dvh max-w-2xl flex-col gap-5 px-4 py-8 text-slate-100">
<header className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-4">
<h1 className="font-bold text-2xl tracking-tight">Idlegame</h1>
<div className="flex items-center gap-2">
<button
type="button"
aria-label={storyHasUnread ? 'Story, unread' : 'Story'}
onClick={() => gameRuntime.openStoryPanel()}
className="relative rounded-lg border border-slate-700 bg-slate-800/70 px-3 py-1.5 text-sm transition-colors hover:border-amber-500/60"
>
Story
{storyHasUnread ? (
<span
className="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-amber-500"
aria-hidden
/>
) : null}
</button>
<button
type="button"
aria-label="Settings"
aria-expanded={settingsOpen}
onClick={() => setSettingsOpen(!settingsOpen)}
className="rounded-lg border border-slate-700 bg-slate-800/70 px-3 py-1.5 text-sm transition-colors hover:border-amber-500/60"
>
</button>
</div>
</div>
<p className="text-slate-500 text-sm">M1 playable loop</p>
<SettingsDrawer />
</header>
<ResourceBar />
<ActionPanel />
<EventLog />
</main>
</>
<AppShell
nav={<NavRail />}
center={renderActivePanel()}
right={activePanel === 'play' ? <RightRail /> : undefined}
/>
);
}