summaryrefslogtreecommitdiff
path: root/makima/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend')
-rw-r--r--makima/frontend/src/components/contracts/ContractContextMenu.tsx168
-rw-r--r--makima/frontend/src/components/workflow/PhaseColumn.tsx3
-rw-r--r--makima/frontend/src/components/workflow/WorkflowBoard.tsx3
-rw-r--r--makima/frontend/src/components/workflow/WorkflowContractCard.tsx11
-rw-r--r--makima/frontend/src/routes/workflow.tsx72
5 files changed, 255 insertions, 2 deletions
diff --git a/makima/frontend/src/components/contracts/ContractContextMenu.tsx b/makima/frontend/src/components/contracts/ContractContextMenu.tsx
new file mode 100644
index 0000000..6e21641
--- /dev/null
+++ b/makima/frontend/src/components/contracts/ContractContextMenu.tsx
@@ -0,0 +1,168 @@
+import { useEffect, useRef } from "react";
+import type { ContractSummary, ContractStatus } from "../../lib/api";
+
+interface ContractContextMenuProps {
+ x: number;
+ y: number;
+ contract: ContractSummary;
+ onClose: () => void;
+ onMarkComplete: (contractId: string) => void;
+ onMarkActive: (contractId: string) => void;
+ onArchive: (contractId: string) => void;
+ onGoToSupervisor: (supervisorTaskId: string) => void;
+ onDelete: (contractId: string) => void;
+}
+
+const statusConfig: Record<ContractStatus, { label: string; color: string }> = {
+ active: { label: "Active", color: "text-green-400" },
+ completed: { label: "Completed", color: "text-blue-400" },
+ archived: { label: "Archived", color: "text-[#555]" },
+};
+
+export function ContractContextMenu({
+ x,
+ y,
+ contract,
+ onClose,
+ onMarkComplete,
+ onMarkActive,
+ onArchive,
+ onGoToSupervisor,
+ onDelete,
+}: ContractContextMenuProps) {
+ const menuRef = useRef<HTMLDivElement>(null);
+
+ // Close on click outside
+ useEffect(() => {
+ const handleClickOutside = (e: MouseEvent) => {
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
+ onClose();
+ }
+ };
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Escape") {
+ onClose();
+ }
+ };
+
+ document.addEventListener("mousedown", handleClickOutside);
+ document.addEventListener("keydown", handleKeyDown);
+
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ document.removeEventListener("keydown", handleKeyDown);
+ };
+ }, [onClose]);
+
+ // Adjust position if menu would overflow viewport
+ useEffect(() => {
+ if (menuRef.current) {
+ const rect = menuRef.current.getBoundingClientRect();
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
+
+ if (rect.right > viewportWidth) {
+ menuRef.current.style.left = `${x - rect.width}px`;
+ }
+ if (rect.bottom > viewportHeight) {
+ menuRef.current.style.top = `${y - rect.height}px`;
+ }
+ }
+ }, [x, y]);
+
+ const status = statusConfig[contract.status] || statusConfig.active;
+
+ const menuItemClass =
+ "w-full px-3 py-1.5 text-left text-xs font-mono text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.1)] flex items-center gap-2";
+ const dividerClass = "border-t border-[rgba(117,170,252,0.2)] my-1";
+
+ return (
+ <div
+ ref={menuRef}
+ className="fixed z-50 bg-[#0a0a0a] border border-[rgba(117,170,252,0.3)] shadow-lg min-w-[180px]"
+ style={{ left: x, top: y }}
+ >
+ {/* Header showing contract info */}
+ <div className="px-3 py-1.5 border-b border-[rgba(117,170,252,0.2)]">
+ <div className="text-[10px] font-mono text-[#555] uppercase truncate">
+ {contract.name}
+ </div>
+ <div className={`text-[10px] font-mono ${status.color}`}>
+ {status.label} | {contract.phase}
+ </div>
+ </div>
+
+ {/* Status actions */}
+ {contract.status !== "completed" && (
+ <button
+ className={menuItemClass}
+ onClick={() => {
+ onMarkComplete(contract.id);
+ onClose();
+ }}
+ >
+ <span className="text-blue-400">&#10003;</span>
+ Mark as Complete
+ </button>
+ )}
+
+ {contract.status !== "active" && (
+ <button
+ className={menuItemClass}
+ onClick={() => {
+ onMarkActive(contract.id);
+ onClose();
+ }}
+ >
+ <span className="text-green-400">&#9654;</span>
+ Mark as Active
+ </button>
+ )}
+
+ {contract.status !== "archived" && (
+ <button
+ className={menuItemClass}
+ onClick={() => {
+ onArchive(contract.id);
+ onClose();
+ }}
+ >
+ <span className="text-[#555]">&#128451;</span>
+ Archive
+ </button>
+ )}
+
+ {/* Supervisor task link */}
+ {contract.supervisorTaskId && (
+ <>
+ <div className={dividerClass} />
+ <button
+ className={menuItemClass}
+ onClick={() => {
+ onGoToSupervisor(contract.supervisorTaskId!);
+ onClose();
+ }}
+ >
+ <span className="text-[#75aafc]">&#9654;</span>
+ Go to Supervisor Task
+ </button>
+ </>
+ )}
+
+ <div className={dividerClass} />
+
+ {/* Delete action */}
+ <button
+ className={`${menuItemClass} text-red-400 hover:bg-red-400/10`}
+ onClick={() => {
+ onDelete(contract.id);
+ onClose();
+ }}
+ >
+ <span className="text-red-400">x</span>
+ Delete
+ </button>
+ </div>
+ );
+}
diff --git a/makima/frontend/src/components/workflow/PhaseColumn.tsx b/makima/frontend/src/components/workflow/PhaseColumn.tsx
index ddea85f..fb15ae3 100644
--- a/makima/frontend/src/components/workflow/PhaseColumn.tsx
+++ b/makima/frontend/src/components/workflow/PhaseColumn.tsx
@@ -7,6 +7,7 @@ interface PhaseColumnProps {
contracts: ContractSummary[];
onContractClick: (contractId: string) => void;
onDrop: (contractId: string, phase: ContractPhase) => void;
+ onContextMenu?: (e: React.MouseEvent, contract: ContractSummary) => void;
}
const phaseConfig: Record<
@@ -50,6 +51,7 @@ export function PhaseColumn({
contracts,
onContractClick,
onDrop,
+ onContextMenu,
}: PhaseColumnProps) {
const [isDragOver, setIsDragOver] = useState(false);
const config = phaseConfig[phase];
@@ -114,6 +116,7 @@ export function PhaseColumn({
e.dataTransfer.setData("contractId", contract.id);
e.dataTransfer.effectAllowed = "move";
}}
+ onContextMenu={onContextMenu}
/>
))
)}
diff --git a/makima/frontend/src/components/workflow/WorkflowBoard.tsx b/makima/frontend/src/components/workflow/WorkflowBoard.tsx
index af4aec7..ee2ff75 100644
--- a/makima/frontend/src/components/workflow/WorkflowBoard.tsx
+++ b/makima/frontend/src/components/workflow/WorkflowBoard.tsx
@@ -6,6 +6,7 @@ interface WorkflowBoardProps {
contracts: ContractSummary[];
onContractClick: (contractId: string) => void;
onPhaseChange: (contractId: string, newPhase: ContractPhase) => void;
+ onContextMenu?: (e: React.MouseEvent, contract: ContractSummary) => void;
}
const phases: ContractPhase[] = ["research", "specify", "plan", "execute", "review"];
@@ -14,6 +15,7 @@ export function WorkflowBoard({
contracts,
onContractClick,
onPhaseChange,
+ onContextMenu,
}: WorkflowBoardProps) {
// Group contracts by phase
const contractsByPhase = useMemo(() => {
@@ -47,6 +49,7 @@ export function WorkflowBoard({
contracts={contractsByPhase[phase]}
onContractClick={onContractClick}
onDrop={onPhaseChange}
+ onContextMenu={onContextMenu}
/>
))}
</div>
diff --git a/makima/frontend/src/components/workflow/WorkflowContractCard.tsx b/makima/frontend/src/components/workflow/WorkflowContractCard.tsx
index 61e6d17..d62f4b0 100644
--- a/makima/frontend/src/components/workflow/WorkflowContractCard.tsx
+++ b/makima/frontend/src/components/workflow/WorkflowContractCard.tsx
@@ -5,6 +5,7 @@ interface WorkflowContractCardProps {
contract: ContractSummary;
onClick: () => void;
onDragStart: (e: React.DragEvent) => void;
+ onContextMenu?: (e: React.MouseEvent, contract: ContractSummary) => void;
}
const statusConfig: Record<ContractStatus, { label: string; color: string }> = {
@@ -17,6 +18,7 @@ export function WorkflowContractCard({
contract,
onClick,
onDragStart,
+ onContextMenu,
}: WorkflowContractCardProps) {
const navigate = useNavigate();
const status = statusConfig[contract.status] || statusConfig.active;
@@ -28,11 +30,20 @@ export function WorkflowContractCard({
}
};
+ const handleContextMenu = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (onContextMenu) {
+ onContextMenu(e, contract);
+ }
+ };
+
return (
<div
draggable
onDragStart={onDragStart}
onClick={onClick}
+ onContextMenu={handleContextMenu}
className="p-3 bg-[rgba(9,13,20,0.8)] border border-[rgba(117,170,252,0.2)] hover:border-[rgba(117,170,252,0.4)] cursor-pointer transition-colors select-none"
>
{/* Header row with name and supervisor button */}
diff --git a/makima/frontend/src/routes/workflow.tsx b/makima/frontend/src/routes/workflow.tsx
index cb72e9e..79148e2 100644
--- a/makima/frontend/src/routes/workflow.tsx
+++ b/makima/frontend/src/routes/workflow.tsx
@@ -2,12 +2,19 @@ import { useState, useCallback, useEffect, useMemo } from "react";
import { useNavigate } from "react-router";
import { Masthead } from "../components/Masthead";
import { WorkflowBoard } from "../components/workflow/WorkflowBoard";
+import { ContractContextMenu } from "../components/contracts/ContractContextMenu";
import { useContracts } from "../hooks/useContracts";
import { useAuth } from "../contexts/AuthContext";
-import type { ContractPhase, ContractStatus } from "../lib/api";
+import type { ContractPhase, ContractStatus, ContractSummary } from "../lib/api";
type StatusFilter = "all" | ContractStatus;
+interface ContextMenuState {
+ x: number;
+ y: number;
+ contract: ContractSummary;
+}
+
export default function WorkflowPage() {
const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth();
const navigate = useNavigate();
@@ -41,10 +48,11 @@ export default function WorkflowPage() {
function WorkflowPageContent() {
const navigate = useNavigate();
- const { contracts, loading, error, changePhase, saveContract } = useContracts();
+ const { contracts, loading, error, changePhase, saveContract, editContract, removeContract } = useContracts();
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all");
const [isCreating, setIsCreating] = useState(false);
const [newContractName, setNewContractName] = useState("");
+ const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
// Filter contracts by status
const filteredContracts = useMemo(() => {
@@ -85,6 +93,50 @@ function WorkflowPageContent() {
setIsCreating(false);
}, []);
+ // Context menu handlers
+ const handleContextMenu = useCallback((e: React.MouseEvent, contract: ContractSummary) => {
+ setContextMenu({
+ x: e.clientX,
+ y: e.clientY,
+ contract,
+ });
+ }, []);
+
+ const handleCloseContextMenu = useCallback(() => {
+ setContextMenu(null);
+ }, []);
+
+ const handleMarkComplete = useCallback(async (contractId: string) => {
+ const contract = contracts.find(c => c.id === contractId);
+ if (contract) {
+ await editContract(contractId, { status: "completed", version: contract.version });
+ }
+ }, [contracts, editContract]);
+
+ const handleMarkActive = useCallback(async (contractId: string) => {
+ const contract = contracts.find(c => c.id === contractId);
+ if (contract) {
+ await editContract(contractId, { status: "active", version: contract.version });
+ }
+ }, [contracts, editContract]);
+
+ const handleArchive = useCallback(async (contractId: string) => {
+ const contract = contracts.find(c => c.id === contractId);
+ if (contract) {
+ await editContract(contractId, { status: "archived", version: contract.version });
+ }
+ }, [contracts, editContract]);
+
+ const handleGoToSupervisor = useCallback((supervisorTaskId: string) => {
+ navigate(`/mesh/${supervisorTaskId}`);
+ }, [navigate]);
+
+ const handleDelete = useCallback(async (contractId: string) => {
+ if (confirm("Are you sure you want to delete this contract?")) {
+ await removeContract(contractId);
+ }
+ }, [removeContract]);
+
return (
<div className="relative z-10 h-screen flex flex-col bg-[#0a1628]">
<Masthead showNav />
@@ -196,10 +248,26 @@ function WorkflowPageContent() {
contracts={filteredContracts}
onContractClick={handleContractClick}
onPhaseChange={handlePhaseChange}
+ onContextMenu={handleContextMenu}
/>
)}
</div>
</main>
+
+ {/* Context Menu */}
+ {contextMenu && (
+ <ContractContextMenu
+ x={contextMenu.x}
+ y={contextMenu.y}
+ contract={contextMenu.contract}
+ onClose={handleCloseContextMenu}
+ onMarkComplete={handleMarkComplete}
+ onMarkActive={handleMarkActive}
+ onArchive={handleArchive}
+ onGoToSupervisor={handleGoToSupervisor}
+ onDelete={handleDelete}
+ />
+ )}
</div>
);
}