summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/workflow
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-11 05:52:14 +0000
committersoryu <soryu@soryu.co>2026-01-15 00:21:16 +0000
commit87044a747b47bd83249d61a45842c7f7b2eae56d (patch)
treeef2000ce79ffcc2723ef841acef5aa1deb1d5378 /makima/frontend/src/components/workflow
parent077820c4167c168072d217a1b01df840463a12a8 (diff)
downloadsoryu-87044a747b47bd83249d61a45842c7f7b2eae56d.tar.gz
soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.zip
Contract system
Diffstat (limited to 'makima/frontend/src/components/workflow')
-rw-r--r--makima/frontend/src/components/workflow/PhaseColumn.tsx123
-rw-r--r--makima/frontend/src/components/workflow/WorkflowBoard.tsx54
-rw-r--r--makima/frontend/src/components/workflow/WorkflowContractCard.tsx53
3 files changed, 230 insertions, 0 deletions
diff --git a/makima/frontend/src/components/workflow/PhaseColumn.tsx b/makima/frontend/src/components/workflow/PhaseColumn.tsx
new file mode 100644
index 0000000..ddea85f
--- /dev/null
+++ b/makima/frontend/src/components/workflow/PhaseColumn.tsx
@@ -0,0 +1,123 @@
+import { useState } from "react";
+import type { ContractSummary, ContractPhase } from "../../lib/api";
+import { WorkflowContractCard } from "./WorkflowContractCard";
+
+interface PhaseColumnProps {
+ phase: ContractPhase;
+ contracts: ContractSummary[];
+ onContractClick: (contractId: string) => void;
+ onDrop: (contractId: string, phase: ContractPhase) => void;
+}
+
+const phaseConfig: Record<
+ ContractPhase,
+ { label: string; color: string; bgColor: string; borderColor: string }
+> = {
+ research: {
+ label: "Research",
+ color: "text-purple-400",
+ bgColor: "bg-purple-400/10",
+ borderColor: "border-purple-400/30",
+ },
+ specify: {
+ label: "Specify",
+ color: "text-blue-400",
+ bgColor: "bg-blue-400/10",
+ borderColor: "border-blue-400/30",
+ },
+ plan: {
+ label: "Plan",
+ color: "text-cyan-400",
+ bgColor: "bg-cyan-400/10",
+ borderColor: "border-cyan-400/30",
+ },
+ execute: {
+ label: "Execute",
+ color: "text-yellow-400",
+ bgColor: "bg-yellow-400/10",
+ borderColor: "border-yellow-400/30",
+ },
+ review: {
+ label: "Review",
+ color: "text-green-400",
+ bgColor: "bg-green-400/10",
+ borderColor: "border-green-400/30",
+ },
+};
+
+export function PhaseColumn({
+ phase,
+ contracts,
+ onContractClick,
+ onDrop,
+}: PhaseColumnProps) {
+ const [isDragOver, setIsDragOver] = useState(false);
+ const config = phaseConfig[phase];
+
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragOver(true);
+ };
+
+ const handleDragLeave = () => {
+ setIsDragOver(false);
+ };
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragOver(false);
+ const contractId = e.dataTransfer.getData("contractId");
+ if (contractId) {
+ onDrop(contractId, phase);
+ }
+ };
+
+ return (
+ <div
+ className={`
+ flex flex-col min-w-[220px] flex-1 border border-[rgba(117,170,252,0.15)]
+ ${isDragOver ? "bg-[rgba(117,170,252,0.05)]" : "bg-transparent"}
+ transition-colors
+ `}
+ onDragOver={handleDragOver}
+ onDragLeave={handleDragLeave}
+ onDrop={handleDrop}
+ >
+ {/* Column header */}
+ <div
+ className={`
+ p-3 border-b ${config.borderColor} ${config.bgColor}
+ flex items-center justify-between
+ `}
+ >
+ <span className={`font-mono text-xs uppercase tracking-wider ${config.color}`}>
+ {config.label}
+ </span>
+ <span className="font-mono text-[10px] text-[#555]">
+ ({contracts.length})
+ </span>
+ </div>
+
+ {/* Cards container */}
+ <div className="flex-1 overflow-y-auto p-2 space-y-2">
+ {contracts.length === 0 ? (
+ <div className="p-4 text-center font-mono text-[10px] text-[#555]">
+ No contracts
+ </div>
+ ) : (
+ contracts.map((contract) => (
+ <WorkflowContractCard
+ key={contract.id}
+ contract={contract}
+ onClick={() => onContractClick(contract.id)}
+ onDragStart={(e) => {
+ e.dataTransfer.setData("contractId", contract.id);
+ e.dataTransfer.effectAllowed = "move";
+ }}
+ />
+ ))
+ )}
+ </div>
+ </div>
+ );
+}
diff --git a/makima/frontend/src/components/workflow/WorkflowBoard.tsx b/makima/frontend/src/components/workflow/WorkflowBoard.tsx
new file mode 100644
index 0000000..af4aec7
--- /dev/null
+++ b/makima/frontend/src/components/workflow/WorkflowBoard.tsx
@@ -0,0 +1,54 @@
+import { useMemo } from "react";
+import type { ContractSummary, ContractPhase } from "../../lib/api";
+import { PhaseColumn } from "./PhaseColumn";
+
+interface WorkflowBoardProps {
+ contracts: ContractSummary[];
+ onContractClick: (contractId: string) => void;
+ onPhaseChange: (contractId: string, newPhase: ContractPhase) => void;
+}
+
+const phases: ContractPhase[] = ["research", "specify", "plan", "execute", "review"];
+
+export function WorkflowBoard({
+ contracts,
+ onContractClick,
+ onPhaseChange,
+}: WorkflowBoardProps) {
+ // Group contracts by phase
+ const contractsByPhase = useMemo(() => {
+ const grouped: Record<ContractPhase, ContractSummary[]> = {
+ research: [],
+ specify: [],
+ plan: [],
+ execute: [],
+ review: [],
+ };
+
+ for (const contract of contracts) {
+ const phase = contract.phase as ContractPhase;
+ if (grouped[phase]) {
+ grouped[phase].push(contract);
+ } else {
+ // Default to research if unknown phase
+ grouped.research.push(contract);
+ }
+ }
+
+ return grouped;
+ }, [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}
+ />
+ ))}
+ </div>
+ );
+}
diff --git a/makima/frontend/src/components/workflow/WorkflowContractCard.tsx b/makima/frontend/src/components/workflow/WorkflowContractCard.tsx
new file mode 100644
index 0000000..e6c8a1c
--- /dev/null
+++ b/makima/frontend/src/components/workflow/WorkflowContractCard.tsx
@@ -0,0 +1,53 @@
+import type { ContractSummary, ContractStatus } from "../../lib/api";
+
+interface WorkflowContractCardProps {
+ contract: ContractSummary;
+ onClick: () => void;
+ onDragStart: (e: React.DragEvent) => void;
+}
+
+const statusConfig: Record<ContractStatus, { label: string; color: string }> = {
+ active: { label: "Active", color: "text-green-400" },
+ completed: { label: "Done", color: "text-blue-400" },
+ archived: { label: "Archived", color: "text-[#555]" },
+};
+
+export function WorkflowContractCard({
+ contract,
+ onClick,
+ onDragStart,
+}: WorkflowContractCardProps) {
+ const status = statusConfig[contract.status] || statusConfig.active;
+
+ return (
+ <div
+ draggable
+ onDragStart={onDragStart}
+ onClick={onClick}
+ 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"
+ >
+ {/* Name */}
+ <div className="font-mono text-sm text-[#dbe7ff] truncate mb-1">
+ {contract.name}
+ </div>
+
+ {/* Status and counts row */}
+ <div className="flex items-center justify-between">
+ <span className={`font-mono text-[10px] uppercase ${status.color}`}>
+ {status.label}
+ </span>
+ <div className="flex items-center gap-2 font-mono text-[10px] text-[#555]">
+ <span title="Files">{contract.fileCount} files</span>
+ <span title="Tasks">{contract.taskCount} tasks</span>
+ </div>
+ </div>
+
+ {/* Description preview if exists */}
+ {contract.description && (
+ <div className="mt-1 font-mono text-[10px] text-[#555] truncate">
+ {contract.description}
+ </div>
+ )}
+ </div>
+ );
+}