summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-19 20:05:28 +0000
committersoryu <soryu@soryu.co>2026-01-19 20:05:28 +0000
commit79ece9163afdb2d1c692f3c6e27a07dcc7a6e5b0 (patch)
tree03945695425261df9a3448928468b64a02bf4a9d
parent307d261dacb8520f9c6ca58bcee2d38a2346acd7 (diff)
parent9d418d4dabef392b16fd79ee84fa7c3214bfcbe3 (diff)
downloadsoryu-79ece9163afdb2d1c692f3c6e27a07dcc7a6e5b0.tar.gz
soryu-79ece9163afdb2d1c692f3c6e27a07dcc7a6e5b0.zip
Fix context menu for workflow board contracts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
-rw-r--r--makima/frontend/src/components/contracts/ContractContextMenu.tsx70
-rw-r--r--makima/frontend/src/components/workflow/PhaseColumn.tsx2
-rw-r--r--makima/frontend/src/components/workflow/WorkflowBoard.tsx69
-rw-r--r--makima/frontend/src/components/workflow/WorkflowContractCard.tsx12
-rw-r--r--makima/frontend/src/routes/workflow.tsx113
5 files changed, 132 insertions, 134 deletions
diff --git a/makima/frontend/src/components/contracts/ContractContextMenu.tsx b/makima/frontend/src/components/contracts/ContractContextMenu.tsx
index f31beb5..6e21641 100644
--- a/makima/frontend/src/components/contracts/ContractContextMenu.tsx
+++ b/makima/frontend/src/components/contracts/ContractContextMenu.tsx
@@ -1,18 +1,24 @@
import { useEffect, useRef } from "react";
-import type { ContractSummary } from "../../lib/api";
+import type { ContractSummary, ContractStatus } from "../../lib/api";
interface ContractContextMenuProps {
x: number;
y: number;
contract: ContractSummary;
onClose: () => void;
- onMarkComplete: () => void;
- onMarkActive: () => void;
- onArchive: () => void;
- onDelete: () => void;
- onGoToSupervisor: () => 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,
@@ -21,8 +27,8 @@ export function ContractContextMenu({
onMarkComplete,
onMarkActive,
onArchive,
- onDelete,
onGoToSupervisor,
+ onDelete,
}: ContractContextMenuProps) {
const menuRef = useRef<HTMLDivElement>(null);
@@ -65,78 +71,80 @@ export function ContractContextMenu({
}
}, [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";
- const showMarkComplete = contract.status !== "completed";
- const showMarkActive = contract.status !== "active";
- const showArchive = contract.status !== "archived";
- const showGoToSupervisor = !!contract.supervisorTaskId;
-
return (
<div
ref={menuRef}
- className="fixed z-50 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] shadow-lg min-w-[180px]"
+ 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 name */}
- <div className="px-3 py-1.5 text-[10px] font-mono text-[#555] uppercase border-b border-[rgba(117,170,252,0.2)] truncate max-w-[200px]">
- {contract.name}
+ {/* 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 */}
- {showMarkComplete && (
+ {contract.status !== "completed" && (
<button
className={menuItemClass}
onClick={() => {
- onMarkComplete();
+ onMarkComplete(contract.id);
onClose();
}}
>
- <span className="text-[#75aafc]">✓</span>
+ <span className="text-blue-400">&#10003;</span>
Mark as Complete
</button>
)}
- {showMarkActive && (
+ {contract.status !== "active" && (
<button
className={menuItemClass}
onClick={() => {
- onMarkActive();
+ onMarkActive(contract.id);
onClose();
}}
>
- <span className="text-[#75aafc]">●</span>
+ <span className="text-green-400">&#9654;</span>
Mark as Active
</button>
)}
- {showArchive && (
+ {contract.status !== "archived" && (
<button
className={menuItemClass}
onClick={() => {
- onArchive();
+ onArchive(contract.id);
onClose();
}}
>
- <span className="text-[#75aafc]">▣</span>
+ <span className="text-[#555]">&#128451;</span>
Archive
</button>
)}
- {/* Supervisor link */}
- {showGoToSupervisor && (
+ {/* Supervisor task link */}
+ {contract.supervisorTaskId && (
<>
<div className={dividerClass} />
<button
className={menuItemClass}
onClick={() => {
- onGoToSupervisor();
+ onGoToSupervisor(contract.supervisorTaskId!);
onClose();
}}
>
- <span className="text-[#75aafc]">▶</span>
+ <span className="text-[#75aafc]">&#9654;</span>
Go to Supervisor Task
</button>
</>
@@ -148,11 +156,11 @@ export function ContractContextMenu({
<button
className={`${menuItemClass} text-red-400 hover:bg-red-400/10`}
onClick={() => {
- onDelete();
+ onDelete(contract.id);
onClose();
}}
>
- <span className="text-red-400">✕</span>
+ <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 277b04c..fb15ae3 100644
--- a/makima/frontend/src/components/workflow/PhaseColumn.tsx
+++ b/makima/frontend/src/components/workflow/PhaseColumn.tsx
@@ -116,7 +116,7 @@ export function PhaseColumn({
e.dataTransfer.setData("contractId", contract.id);
e.dataTransfer.effectAllowed = "move";
}}
- onContextMenu={onContextMenu ? (e) => onContextMenu(e, contract) : undefined}
+ onContextMenu={onContextMenu}
/>
))
)}
diff --git a/makima/frontend/src/components/workflow/WorkflowBoard.tsx b/makima/frontend/src/components/workflow/WorkflowBoard.tsx
index e36ca21..ee2ff75 100644
--- a/makima/frontend/src/components/workflow/WorkflowBoard.tsx
+++ b/makima/frontend/src/components/workflow/WorkflowBoard.tsx
@@ -1,17 +1,12 @@
-import { useMemo, useState } from "react";
+import { useMemo } from "react";
import type { ContractSummary, ContractPhase } from "../../lib/api";
import { PhaseColumn } from "./PhaseColumn";
-import { ContractContextMenu } from "../contracts/ContractContextMenu";
interface WorkflowBoardProps {
contracts: ContractSummary[];
onContractClick: (contractId: string) => void;
onPhaseChange: (contractId: string, newPhase: ContractPhase) => void;
- onMarkComplete?: (contract: ContractSummary) => void;
- onMarkActive?: (contract: ContractSummary) => void;
- onArchive?: (contract: ContractSummary) => void;
- onDelete?: (contract: ContractSummary) => void;
- onGoToSupervisor?: (contract: ContractSummary) => void;
+ onContextMenu?: (e: React.MouseEvent, contract: ContractSummary) => void;
}
const phases: ContractPhase[] = ["research", "specify", "plan", "execute", "review"];
@@ -20,27 +15,8 @@ export function WorkflowBoard({
contracts,
onContractClick,
onPhaseChange,
- onMarkComplete,
- onMarkActive,
- onArchive,
- onDelete,
- onGoToSupervisor,
+ onContextMenu,
}: WorkflowBoardProps) {
- const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null);
- const [contextMenuContract, setContextMenuContract] = useState<ContractSummary | null>(null);
-
- const handleContextMenu = (e: React.MouseEvent, contract: ContractSummary) => {
- e.preventDefault();
- e.stopPropagation(); // Prevent interference with drag-and-drop
- setContextMenuPosition({ x: e.clientX, y: e.clientY });
- setContextMenuContract(contract);
- };
-
- const closeContextMenu = () => {
- setContextMenuPosition(null);
- setContextMenuContract(null);
- };
-
// Group contracts by phase
const contractsByPhase = useMemo(() => {
const grouped: Record<ContractPhase, ContractSummary[]> = {
@@ -65,34 +41,17 @@ export function WorkflowBoard({
}, [contracts]);
return (
- <>
- <div className="flex gap-2 h-full overflow-x-auto">
- {phases.map((phase) => (
- <PhaseColumn
- key={phase}
- phase={phase}
- contracts={contractsByPhase[phase]}
- onContractClick={onContractClick}
- onDrop={onPhaseChange}
- onContextMenu={handleContextMenu}
- />
- ))}
- </div>
-
- {/* Context Menu */}
- {contextMenuPosition && contextMenuContract && (
- <ContractContextMenu
- x={contextMenuPosition.x}
- y={contextMenuPosition.y}
- contract={contextMenuContract}
- onClose={closeContextMenu}
- onMarkComplete={() => onMarkComplete?.(contextMenuContract)}
- onMarkActive={() => onMarkActive?.(contextMenuContract)}
- onArchive={() => onArchive?.(contextMenuContract)}
- onDelete={() => onDelete?.(contextMenuContract)}
- onGoToSupervisor={() => onGoToSupervisor?.(contextMenuContract)}
+ <div className="flex gap-2 h-full overflow-x-auto">
+ {phases.map((phase) => (
+ <PhaseColumn
+ key={phase}
+ phase={phase}
+ 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 86fcd13..d62f4b0 100644
--- a/makima/frontend/src/components/workflow/WorkflowContractCard.tsx
+++ b/makima/frontend/src/components/workflow/WorkflowContractCard.tsx
@@ -5,7 +5,7 @@ interface WorkflowContractCardProps {
contract: ContractSummary;
onClick: () => void;
onDragStart: (e: React.DragEvent) => void;
- onContextMenu?: (e: React.MouseEvent) => void;
+ onContextMenu?: (e: React.MouseEvent, contract: ContractSummary) => void;
}
const statusConfig: Record<ContractStatus, { label: string; color: string }> = {
@@ -30,12 +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={onContextMenu}
+ 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 e122092..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, 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();
@@ -45,6 +52,7 @@ function WorkflowPageContent() {
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(() => {
@@ -68,46 +76,6 @@ function WorkflowPageContent() {
[changePhase]
);
- // Context menu handlers
- const handleContextMarkComplete = useCallback(
- async (contract: ContractSummary) => {
- await editContract(contract.id, { status: "completed", version: contract.version });
- },
- [editContract]
- );
-
- const handleContextMarkActive = useCallback(
- async (contract: ContractSummary) => {
- await editContract(contract.id, { status: "active", version: contract.version });
- },
- [editContract]
- );
-
- const handleContextArchive = useCallback(
- async (contract: ContractSummary) => {
- await editContract(contract.id, { status: "archived", version: contract.version });
- },
- [editContract]
- );
-
- const handleContextDelete = useCallback(
- async (contract: ContractSummary) => {
- if (confirm(`Are you sure you want to delete "${contract.name}"?`)) {
- await removeContract(contract.id);
- }
- },
- [removeContract]
- );
-
- const handleContextGoToSupervisor = useCallback(
- (contract: ContractSummary) => {
- if (contract.supervisorTaskId) {
- navigate(`/mesh/${contract.supervisorTaskId}`);
- }
- },
- [navigate]
- );
-
const handleCreateContract = useCallback(async () => {
if (!newContractName.trim()) return;
const contract = await saveContract({
@@ -125,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 />
@@ -236,15 +248,26 @@ function WorkflowPageContent() {
contracts={filteredContracts}
onContractClick={handleContractClick}
onPhaseChange={handlePhaseChange}
- onMarkComplete={handleContextMarkComplete}
- onMarkActive={handleContextMarkActive}
- onArchive={handleContextArchive}
- onDelete={handleContextDelete}
- onGoToSupervisor={handleContextGoToSupervisor}
+ 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>
);
}