feat(ui): automation bar with recipe export import
This commit is contained in:
@@ -148,6 +148,22 @@ export function ActionCard({ action }: ActionCardProps) {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{action.automationUnlocked ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={`${action.inAutomationQueue ? 'Disable' : 'Enable'} automation for ${action.name}`}
|
||||||
|
aria-pressed={action.inAutomationQueue}
|
||||||
|
title={`${action.inAutomationQueue ? 'Disable' : 'Enable'} automation for ${action.name}`}
|
||||||
|
onClick={() => gameRuntime.toggleAutomation(action.id)}
|
||||||
|
className={`shrink-0 rounded-lg border px-2 text-xs font-semibold transition-colors ${
|
||||||
|
action.inAutomationQueue
|
||||||
|
? 'border-amber-500 bg-amber-500/15 text-amber-300 hover:bg-amber-500/20'
|
||||||
|
: 'border-slate-700 bg-slate-800/70 text-slate-400 hover:border-amber-500/60 hover:text-amber-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Auto
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { gameRuntime } from '../state/runtime';
|
||||||
|
import { useGameStore } from '../state/store';
|
||||||
|
|
||||||
|
export function AutomationBar() {
|
||||||
|
const automationQueueIds = useGameStore((s) => s.automationQueueIds);
|
||||||
|
const automationQueueNames = useGameStore((s) => s.automationQueueNames);
|
||||||
|
const [importText, setImportText] = useState('');
|
||||||
|
|
||||||
|
async function copyRecipe() {
|
||||||
|
const text = gameRuntime.exportAutomationRecipe();
|
||||||
|
try {
|
||||||
|
await navigator.clipboard?.writeText(text);
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
// Fall through to the legacy selection path.
|
||||||
|
}
|
||||||
|
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = text;
|
||||||
|
textarea.setAttribute('readonly', 'true');
|
||||||
|
textarea.style.position = 'fixed';
|
||||||
|
textarea.style.opacity = '0';
|
||||||
|
document.body.append(textarea);
|
||||||
|
textarea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
textarea.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function importRecipeText() {
|
||||||
|
gameRuntime.importAutomationRecipe(importText);
|
||||||
|
setImportText('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
aria-label="Automation"
|
||||||
|
className="rounded-xl border border-slate-800 bg-slate-900/30 p-4"
|
||||||
|
>
|
||||||
|
<div className="mb-3 flex items-center justify-between gap-3">
|
||||||
|
<h3 className="font-semibold text-slate-400 text-xs uppercase tracking-wider">
|
||||||
|
Automation
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={copyRecipe}
|
||||||
|
className="rounded border border-slate-700 px-2.5 py-1 text-slate-300 text-xs transition-colors hover:border-amber-500/60 hover:text-amber-300"
|
||||||
|
>
|
||||||
|
Copy recipe
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{automationQueueNames.length > 0 ? (
|
||||||
|
<ol aria-label="Automation queue" className="mb-3 flex flex-col gap-1.5">
|
||||||
|
{automationQueueNames.map((name, index) => (
|
||||||
|
<li
|
||||||
|
// biome-ignore lint/suspicious/noArrayIndexKey: indices distinguish repeated automation entries in display order
|
||||||
|
key={`${automationQueueIds[index]}-${index}`}
|
||||||
|
className="rounded-lg border border-slate-700 bg-slate-900/60 px-3 py-2 text-slate-300 text-sm"
|
||||||
|
>
|
||||||
|
{index + 1}. {name}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
) : (
|
||||||
|
<p className="mb-3 text-slate-500 text-sm">No automated processes.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<textarea
|
||||||
|
aria-label="Recipe text"
|
||||||
|
className="min-h-24 w-full resize-y rounded-lg border border-slate-700 bg-slate-950/70 p-2 text-slate-200 text-xs outline-none transition-colors placeholder:text-slate-600 focus:border-amber-500/70"
|
||||||
|
placeholder="Paste recipe text..."
|
||||||
|
value={importText}
|
||||||
|
onChange={(event) => setImportText(event.target.value)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={importText.trim().length === 0}
|
||||||
|
onClick={importRecipeText}
|
||||||
|
className="self-start rounded border border-slate-700 px-3 py-1.5 text-slate-300 text-xs transition-colors hover:border-amber-500/60 hover:text-amber-300 disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
|
>
|
||||||
|
Import recipe
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { gameRuntime } from '../state/runtime';
|
import { gameRuntime } from '../state/runtime';
|
||||||
import { useGameStore } from '../state/store';
|
import { useGameStore } from '../state/store';
|
||||||
import { ActionColumn } from './ActionColumn';
|
import { ActionColumn } from './ActionColumn';
|
||||||
|
import { AutomationBar } from './AutomationBar';
|
||||||
import { EventLog } from './EventLog';
|
import { EventLog } from './EventLog';
|
||||||
import { ResourceBar } from './ResourceBar';
|
import { ResourceBar } from './ResourceBar';
|
||||||
|
|
||||||
@@ -29,6 +30,8 @@ export function PlayPanel() {
|
|||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<AutomationBar />
|
||||||
|
|
||||||
{queuedActionNames.length > 0 ? (
|
{queuedActionNames.length > 0 ? (
|
||||||
<div className="border border-slate-800 bg-slate-900/30 rounded-xl p-4">
|
<div className="border border-slate-800 bg-slate-900/30 rounded-xl p-4">
|
||||||
<h3 className="font-semibold text-slate-400 text-xs uppercase tracking-wider mb-2">
|
<h3 className="font-semibold text-slate-400 text-xs uppercase tracking-wider mb-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user