feat: bootstrap walking skeleton
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { content } from '../content';
|
||||
import { gameRuntime } from '../state/runtime';
|
||||
import { useGameStore } from '../state/store';
|
||||
|
||||
/** Action list — a start button per action, with a progress bar on the active one. */
|
||||
export function ActionPanel() {
|
||||
const activeActionId = useGameStore((state) => state.activeActionId);
|
||||
const actionProgress = useGameStore((state) => state.actionProgress);
|
||||
|
||||
return (
|
||||
<section aria-label="Actions" className="flex flex-col gap-2">
|
||||
<h2 className="font-medium text-slate-300 text-sm uppercase tracking-wide">Actions</h2>
|
||||
{content.actions.map((action) => {
|
||||
const isActive = action.id === activeActionId;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={action.id}
|
||||
onClick={() => gameRuntime.startAction(action.id)}
|
||||
className="relative overflow-hidden rounded-lg border border-slate-700 bg-slate-800/70 px-4 py-3 text-left transition-colors hover:border-amber-500/60 hover:bg-slate-800"
|
||||
>
|
||||
{isActive ? (
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 bg-amber-500/15"
|
||||
style={{ width: `${Math.round(actionProgress * 100)}%` }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
<div className="relative flex items-center justify-between">
|
||||
<span className="font-medium text-slate-100">{action.name}</span>
|
||||
<span className="text-slate-400 text-xs">{isActive ? 'running…' : 'start'}</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useEffect } from 'react';
|
||||
import { gameRuntime } from '../state/runtime';
|
||||
import { ActionPanel } from './ActionPanel';
|
||||
import { EventLog } from './EventLog';
|
||||
import { ResourceBar } from './ResourceBar';
|
||||
|
||||
/**
|
||||
* Walking-skeleton view shell. Boots the runtime once on mount; everything else
|
||||
* renders from the Zustand store the runtime feeds. M1 turns this into a game.
|
||||
*/
|
||||
export function App() {
|
||||
useEffect(() => {
|
||||
void gameRuntime.boot();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<main className="mx-auto flex min-h-dvh max-w-2xl flex-col gap-5 px-4 py-8 text-slate-100">
|
||||
<header>
|
||||
<h1 className="font-bold text-2xl tracking-tight">Idlegame</h1>
|
||||
<p className="text-slate-500 text-sm">Walking skeleton — M0 scaffold.</p>
|
||||
</header>
|
||||
<ResourceBar />
|
||||
<ActionPanel />
|
||||
<EventLog />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useGameStore } from '../state/store';
|
||||
|
||||
/** Event log stub — newest entries at the bottom. */
|
||||
export function EventLog() {
|
||||
const log = useGameStore((state) => state.log);
|
||||
|
||||
return (
|
||||
<section aria-label="Event log" className="flex flex-col gap-1">
|
||||
<h2 className="font-medium text-slate-300 text-sm uppercase tracking-wide">Log</h2>
|
||||
<ol className="flex max-h-48 flex-col gap-1 overflow-y-auto rounded-lg border border-slate-800 bg-slate-900/60 p-3 text-sm">
|
||||
{log.length === 0 ? (
|
||||
<li className="text-slate-500">…</li>
|
||||
) : (
|
||||
log.map((line, index) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: log is append-only; index is stable
|
||||
<li key={index} className="text-slate-400">
|
||||
{line}
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ol>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { formatNum } from '../engine/num';
|
||||
import { useGameStore } from '../state/store';
|
||||
|
||||
/** Resource readout — name + human-readable amount for each resource. */
|
||||
export function ResourceBar() {
|
||||
const resources = useGameStore((state) => state.resources);
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label="Resources"
|
||||
className="flex flex-wrap gap-3 rounded-lg border border-slate-800 bg-slate-900/60 p-3"
|
||||
>
|
||||
{resources.length === 0 ? (
|
||||
<span className="text-slate-500 text-sm">Loading…</span>
|
||||
) : (
|
||||
resources.map((resource) => (
|
||||
<div key={resource.id} className="flex items-baseline gap-2">
|
||||
<span className="text-slate-400 text-sm">{resource.name}</span>
|
||||
<span className="font-semibold text-amber-400 tabular-nums">
|
||||
{formatNum(resource.amount)}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user