summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/contracts/PhaseProgressBar.tsx
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/contracts/PhaseProgressBar.tsx
parent077820c4167c168072d217a1b01df840463a12a8 (diff)
downloadsoryu-87044a747b47bd83249d61a45842c7f7b2eae56d.tar.gz
soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.zip
Contract system
Diffstat (limited to 'makima/frontend/src/components/contracts/PhaseProgressBar.tsx')
-rw-r--r--makima/frontend/src/components/contracts/PhaseProgressBar.tsx142
1 files changed, 142 insertions, 0 deletions
diff --git a/makima/frontend/src/components/contracts/PhaseProgressBar.tsx b/makima/frontend/src/components/contracts/PhaseProgressBar.tsx
new file mode 100644
index 0000000..5ee7999
--- /dev/null
+++ b/makima/frontend/src/components/contracts/PhaseProgressBar.tsx
@@ -0,0 +1,142 @@
+import type { ContractPhase } from "../../lib/api";
+
+interface PhaseProgressBarProps {
+ currentPhase: ContractPhase;
+ onPhaseClick?: (phase: ContractPhase) => void;
+ readonly?: boolean;
+}
+
+const phases: ContractPhase[] = ["research", "specify", "plan", "execute", "review"];
+
+const phaseLabels: Record<ContractPhase, string> = {
+ research: "Research",
+ specify: "Specify",
+ plan: "Plan",
+ execute: "Execute",
+ review: "Review",
+};
+
+const phaseColors: Record<ContractPhase, { active: string; inactive: string; completed: string }> = {
+ research: {
+ active: "bg-purple-400 border-purple-400",
+ inactive: "bg-transparent border-purple-400/30",
+ completed: "bg-purple-400/50 border-purple-400/50",
+ },
+ specify: {
+ active: "bg-blue-400 border-blue-400",
+ inactive: "bg-transparent border-blue-400/30",
+ completed: "bg-blue-400/50 border-blue-400/50",
+ },
+ plan: {
+ active: "bg-cyan-400 border-cyan-400",
+ inactive: "bg-transparent border-cyan-400/30",
+ completed: "bg-cyan-400/50 border-cyan-400/50",
+ },
+ execute: {
+ active: "bg-yellow-400 border-yellow-400",
+ inactive: "bg-transparent border-yellow-400/30",
+ completed: "bg-yellow-400/50 border-yellow-400/50",
+ },
+ review: {
+ active: "bg-green-400 border-green-400",
+ inactive: "bg-transparent border-green-400/30",
+ completed: "bg-green-400/50 border-green-400/50",
+ },
+};
+
+export function PhaseProgressBar({
+ currentPhase,
+ onPhaseClick,
+ readonly = false,
+}: PhaseProgressBarProps) {
+ const currentIndex = phases.indexOf(currentPhase);
+
+ return (
+ <div className="flex items-center gap-1">
+ {phases.map((phase, index) => {
+ const isActive = phase === currentPhase;
+ const isCompleted = index < currentIndex;
+ const colors = phaseColors[phase];
+ const colorClass = isActive
+ ? colors.active
+ : isCompleted
+ ? colors.completed
+ : colors.inactive;
+
+ const canClick = !readonly && onPhaseClick;
+
+ return (
+ <div key={phase} className="flex items-center">
+ {/* Phase node */}
+ <button
+ onClick={() => canClick && onPhaseClick(phase)}
+ disabled={readonly}
+ className={`
+ relative group flex flex-col items-center
+ ${canClick ? "cursor-pointer" : "cursor-default"}
+ `}
+ >
+ {/* Circle */}
+ <div
+ className={`
+ w-3 h-3 rounded-full border-2 transition-all
+ ${colorClass}
+ ${canClick && !isActive ? "hover:scale-110" : ""}
+ `}
+ />
+ {/* Label */}
+ <span
+ className={`
+ absolute top-4 font-mono text-[9px] uppercase tracking-wide whitespace-nowrap
+ ${isActive ? "text-[#dbe7ff]" : "text-[#555]"}
+ ${canClick && !isActive ? "group-hover:text-[#75aafc]" : ""}
+ `}
+ >
+ {phaseLabels[phase]}
+ </span>
+ </button>
+
+ {/* Connector line */}
+ {index < phases.length - 1 && (
+ <div
+ className={`
+ w-8 h-0.5 mx-1
+ ${index < currentIndex ? "bg-[#3f6fb3]" : "bg-[rgba(117,170,252,0.15)]"}
+ `}
+ />
+ )}
+ </div>
+ );
+ })}
+ </div>
+ );
+}
+
+export function PhaseProgressBarCompact({
+ currentPhase,
+}: {
+ currentPhase: ContractPhase;
+}) {
+ const currentIndex = phases.indexOf(currentPhase);
+
+ return (
+ <div className="flex items-center gap-0.5">
+ {phases.map((phase, index) => {
+ const isActive = phase === currentPhase;
+ const isCompleted = index < currentIndex;
+ const colors = phaseColors[phase];
+
+ return (
+ <div
+ key={phase}
+ className={`
+ w-2 h-2 rounded-full border
+ ${isActive ? colors.active : isCompleted ? colors.completed : colors.inactive}
+ `}
+ title={phaseLabels[phase]}
+ />
+ );
+ })}
+ </div>
+ );
+}