feat: floating action button for starting tools globally
- New StartToolFAB component: fixed floating button (bottom-right) opens a modal with workspace selector + ToolStarter - Added to AppShell: available on every page except mobile terminal - Dashboard (home): removed old CreateSessionForm and 'Quick create' section, replaced with FAB + 'Workspaces quick access' prompt - Sessions page: removed inline workspace selector + ToolStarter, now shows prompt to use the FAB - Styles: .start-tool-fab with hover scale, shadow, mobile offset above tab bar Quality gates: tsc --noEmit clean, pytest workspaces API (9 passed, 1 skipped)
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
/** Floating action button to start a tool from any page. */
|
||||
|
||||
import { useState } from "react";
|
||||
import { Icon } from "./icon";
|
||||
import { ToolStarter } from "./tool-starter";
|
||||
import type { Workspace } from "../types/workspace";
|
||||
import { listAllWorkspaces } from "../api/workspaces";
|
||||
|
||||
export function StartToolFAB() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
|
||||
const [workspacesLoading, setWorkspacesLoading] = useState(false);
|
||||
const [selectedWorkspace, setSelectedWorkspace] = useState<Workspace | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const handleOpen = async () => {
|
||||
setOpen(true);
|
||||
setWorkspacesLoading(true);
|
||||
try {
|
||||
const data = await listAllWorkspaces();
|
||||
setWorkspaces(data);
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setWorkspacesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
setSelectedWorkspace(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="start-tool-fab"
|
||||
onClick={handleOpen}
|
||||
title="Start a new tool"
|
||||
type="button"
|
||||
aria-label="Start a new tool"
|
||||
>
|
||||
<Icon name="play" size="md" />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="modal-overlay" onClick={handleClose}>
|
||||
<div
|
||||
className="modal-content start-tool-modal"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="modal-header">
|
||||
<h3>Start Tool</h3>
|
||||
<button
|
||||
className="ghost-button small"
|
||||
onClick={handleClose}
|
||||
type="button"
|
||||
aria-label="Close"
|
||||
>
|
||||
<Icon name="close" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{workspacesLoading ? (
|
||||
<p className="muted">Loading workspaces...</p>
|
||||
) : workspaces.length === 0 ? (
|
||||
<p className="muted">
|
||||
No workspaces yet.{" "}
|
||||
<a href="/workspaces">Create a workspace first</a>.
|
||||
</p>
|
||||
) : !selectedWorkspace ? (
|
||||
<div className="form-group">
|
||||
<label htmlFor="fab-workspace-select">
|
||||
Select a workspace
|
||||
</label>
|
||||
<select
|
||||
id="fab-workspace-select"
|
||||
value=""
|
||||
onChange={(e) => {
|
||||
const ws = workspaces.find(
|
||||
(w) => w.id === e.target.value,
|
||||
);
|
||||
if (ws) setSelectedWorkspace(ws);
|
||||
}}
|
||||
>
|
||||
<option value="">Choose a workspace...</option>
|
||||
{workspaces.map((ws) => (
|
||||
<option key={ws.id} value={ws.id}>
|
||||
{ws.project_name} / {ws.repo_name} / {ws.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="tool-starter-header">
|
||||
<h4>
|
||||
{selectedWorkspace.project_name} /{" "}
|
||||
{selectedWorkspace.repo_name} /{" "}
|
||||
{selectedWorkspace.name}
|
||||
</h4>
|
||||
<button
|
||||
className="ghost-button small"
|
||||
onClick={() => setSelectedWorkspace(null)}
|
||||
type="button"
|
||||
>
|
||||
Change
|
||||
</button>
|
||||
</div>
|
||||
<ToolStarter
|
||||
workspace={selectedWorkspace}
|
||||
onStarted={handleClose}
|
||||
onCancel={() => setSelectedWorkspace(null)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user