feat(ui): action queue list, cancel, disabled states, and hints
This commit is contained in:
+83
-9
@@ -1,23 +1,40 @@
|
|||||||
import { content } from '../content';
|
import { useState } from 'react';
|
||||||
import { gameRuntime } from '../state/runtime';
|
import { gameRuntime } from '../state/runtime';
|
||||||
import { useGameStore } from '../state/store';
|
import { useGameStore } from '../state/store';
|
||||||
|
|
||||||
/** Action list — a start button per action, with a progress bar on the active one. */
|
/** Action list with queue, cancel, disabled states, and story hints/tooltips. */
|
||||||
export function ActionPanel() {
|
export function ActionPanel() {
|
||||||
const activeActionId = useGameStore((state) => state.activeActionId);
|
const actions = useGameStore((s) => s.actions);
|
||||||
const actionProgress = useGameStore((state) => state.actionProgress);
|
const activeActionId = useGameStore((s) => s.activeActionId);
|
||||||
|
const actionProgress = useGameStore((s) => s.actionProgress);
|
||||||
|
const queuedActionIds = useGameStore((s) => s.queuedActionIds);
|
||||||
|
const queuedActionNames = useGameStore((s) => s.queuedActionNames);
|
||||||
|
const prefs = useGameStore((s) => s.prefs);
|
||||||
|
const [openInfoId, setOpenInfoId] = useState<string | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section aria-label="Actions" className="flex flex-col gap-2">
|
<section aria-label="Actions" className="flex flex-col gap-2">
|
||||||
<h2 className="font-medium text-slate-300 text-sm uppercase tracking-wide">Actions</h2>
|
<h2 className="font-medium text-slate-300 text-sm uppercase tracking-wide">Actions</h2>
|
||||||
{content.actions.map((action) => {
|
{actions.map((action) => {
|
||||||
const isActive = action.id === activeActionId;
|
const isActive = action.id === activeActionId;
|
||||||
|
const isDisabled = !action.available && !isActive;
|
||||||
|
const summaryParts = [
|
||||||
|
action.costsSummary ? `Cost: ${action.costsSummary}` : null,
|
||||||
|
action.yieldsSummary ? `Yield: ${action.yieldsSummary}` : null,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div key={action.id} className="relative flex gap-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
key={action.id}
|
disabled={isDisabled}
|
||||||
|
title={prefs.actionDetailMode === 'hover' ? action.storyTooltip : undefined}
|
||||||
onClick={() => gameRuntime.enqueueAction(action.id)}
|
onClick={() => gameRuntime.enqueueAction(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"
|
className={`relative w-full overflow-hidden rounded-lg border px-4 py-3 text-left transition-colors ${
|
||||||
|
isDisabled
|
||||||
|
? 'cursor-not-allowed border-slate-800 bg-slate-900/40 opacity-50'
|
||||||
|
: 'border-slate-700 bg-slate-800/70 hover:border-amber-500/60 hover:bg-slate-800'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{isActive ? (
|
{isActive ? (
|
||||||
<div
|
<div
|
||||||
@@ -26,13 +43,70 @@ export function ActionPanel() {
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="relative flex items-center justify-between">
|
<div className="relative flex flex-col gap-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<span className="font-medium text-slate-100">{action.name}</span>
|
<span className="font-medium text-slate-100">{action.name}</span>
|
||||||
<span className="text-slate-400 text-xs">{isActive ? 'running…' : 'start'}</span>
|
<span className="text-slate-400 text-xs">
|
||||||
|
{isActive
|
||||||
|
? 'running…'
|
||||||
|
: isDisabled
|
||||||
|
? (action.disabledReason ?? 'unavailable')
|
||||||
|
: 'start'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{summaryParts.length > 0 ? (
|
||||||
|
<p className="text-slate-500 text-xs">{summaryParts.join(' · ')}</p>
|
||||||
|
) : null}
|
||||||
|
{prefs.actionDetailMode === 'inline' && action.storyHint ? (
|
||||||
|
<p className="text-slate-400 text-xs">{action.storyHint}</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
{prefs.actionDetailMode === 'info-button' && action.storyTooltip ? (
|
||||||
|
<div className="relative shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={`Details for ${action.name}`}
|
||||||
|
aria-expanded={openInfoId === action.id}
|
||||||
|
onClick={() => setOpenInfoId(openInfoId === action.id ? null : action.id)}
|
||||||
|
className="flex h-full items-center rounded-lg border border-slate-700 bg-slate-800/70 px-2 text-slate-400 transition-colors hover:border-amber-500/60 hover:text-slate-200"
|
||||||
|
title={action.storyTooltip}
|
||||||
|
>
|
||||||
|
ⓘ
|
||||||
|
</button>
|
||||||
|
{openInfoId === action.id ? (
|
||||||
|
<div
|
||||||
|
role="tooltip"
|
||||||
|
className="absolute top-full right-0 z-10 mt-1 max-w-xs rounded-lg border border-slate-600 bg-slate-900 p-3 text-slate-300 text-xs shadow-lg"
|
||||||
|
>
|
||||||
|
{action.storyTooltip}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{queuedActionNames.length > 0 ? (
|
||||||
|
<ol aria-label="Action queue" className="mt-2 flex flex-col gap-1">
|
||||||
|
{queuedActionNames.map((name, index) => (
|
||||||
|
<li
|
||||||
|
key={queuedActionIds[index]}
|
||||||
|
className="flex items-center justify-between rounded border border-slate-700 bg-slate-900/60 px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
<span className="text-slate-300">{name}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={`Cancel ${name}`}
|
||||||
|
onClick={() => gameRuntime.cancelQueuedAction(index)}
|
||||||
|
className="rounded px-2 py-0.5 text-slate-400 transition-colors hover:bg-slate-800 hover:text-amber-400"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user