From cf0a25af1d2834bfe6c5ea892ce5769936e5a673 Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
Date: Tue, 3 Feb 2026 22:01:29 +0000
Subject: Add makima chain mechanism

---
 .../frontend/src/components/chains/ChainList.tsx   | 205 +++++++++++++++++++++
 1 file changed, 205 insertions(+)
 create mode 100644 makima/frontend/src/components/chains/ChainList.tsx

(limited to 'makima/frontend/src/components/chains/ChainList.tsx')

diff --git a/makima/frontend/src/components/chains/ChainList.tsx b/makima/frontend/src/components/chains/ChainList.tsx
new file mode 100644
index 0000000..eda79d7
--- /dev/null
+++ b/makima/frontend/src/components/chains/ChainList.tsx
@@ -0,0 +1,205 @@
+import { useState, useCallback } from "react";
+import type { ChainSummary, ChainStatus } from "../../lib/api";
+
+interface ChainListProps {
+  chains: ChainSummary[];
+  loading: boolean;
+  onSelect: (chainId: string) => void;
+  onCreate: () => void;
+  selectedId?: string;
+  onArchive: (chain: ChainSummary) => void;
+}
+
+export function ChainList({
+  chains,
+  loading,
+  onSelect,
+  onCreate,
+  selectedId,
+  onArchive,
+}: ChainListProps) {
+  const [statusFilter, setStatusFilter] = useState<ChainStatus | "all">("all");
+  const [contextMenu, setContextMenu] = useState<{
+    chain: ChainSummary;
+    x: number;
+    y: number;
+  } | null>(null);
+
+  const filteredChains = chains.filter((chain) =>
+    statusFilter === "all" ? true : chain.status === statusFilter
+  );
+
+  const handleContextMenu = useCallback(
+    (e: React.MouseEvent, chain: ChainSummary) => {
+      e.preventDefault();
+      setContextMenu({ chain, x: e.clientX, y: e.clientY });
+    },
+    []
+  );
+
+  const closeContextMenu = useCallback(() => {
+    setContextMenu(null);
+  }, []);
+
+  const handleArchive = useCallback(() => {
+    if (contextMenu) {
+      onArchive(contextMenu.chain);
+      setContextMenu(null);
+    }
+  }, [contextMenu, onArchive]);
+
+  const getStatusColor = (status: ChainStatus) => {
+    switch (status) {
+      case "active":
+        return "text-[#4ade80] bg-[#4ade80]/10";
+      case "completed":
+        return "text-[#60a5fa] bg-[#60a5fa]/10";
+      case "archived":
+        return "text-[#6b7280] bg-[#6b7280]/10";
+      default:
+        return "text-[#8b949e] bg-[#8b949e]/10";
+    }
+  };
+
+  const getStatusIcon = (status: ChainStatus) => {
+    switch (status) {
+      case "active":
+        return (
+          <svg className="w-3 h-3" viewBox="0 0 24 24" fill="currentColor">
+            <circle cx="12" cy="12" r="4" />
+          </svg>
+        );
+      case "completed":
+        return (
+          <svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
+            <polyline points="20 6 9 17 4 12" />
+          </svg>
+        );
+      case "archived":
+        return (
+          <svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+            <path d="M21 8v13H3V8" />
+            <path d="M1 3h22v5H1z" />
+            <path d="M10 12h4" />
+          </svg>
+        );
+      default:
+        return null;
+    }
+  };
+
+  return (
+    <div className="panel h-full flex flex-col" onClick={closeContextMenu}>
+      {/* Header */}
+      <div className="p-3 border-b border-[rgba(117,170,252,0.2)]">
+        <div className="flex items-center justify-between mb-3">
+          <h2 className="font-mono text-sm text-[#75aafc] uppercase">Chains</h2>
+          <button
+            onClick={onCreate}
+            className="px-3 py-1 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors"
+          >
+            + New
+          </button>
+        </div>
+
+        {/* Status filter */}
+        <div className="flex gap-1">
+          {(["all", "active", "completed", "archived"] as const).map((status) => (
+            <button
+              key={status}
+              onClick={() => setStatusFilter(status)}
+              className={`px-2 py-1 font-mono text-[10px] uppercase transition-colors ${
+                statusFilter === status
+                  ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]"
+                  : "bg-[#0d1b2d] text-[#8b949e] border border-[#3f6fb3] hover:border-[#75aafc]"
+              }`}
+            >
+              {status}
+            </button>
+          ))}
+        </div>
+      </div>
+
+      {/* Chain list */}
+      <div className="flex-1 overflow-y-auto">
+        {loading ? (
+          <div className="flex items-center justify-center h-32">
+            <p className="font-mono text-xs text-[#8b949e]">Loading chains...</p>
+          </div>
+        ) : filteredChains.length === 0 ? (
+          <div className="flex items-center justify-center h-32">
+            <p className="font-mono text-xs text-[#8b949e]">
+              {statusFilter === "all" ? "No chains yet" : `No ${statusFilter} chains`}
+            </p>
+          </div>
+        ) : (
+          <div className="divide-y divide-[rgba(117,170,252,0.1)]">
+            {filteredChains.map((chain) => (
+              <div
+                key={chain.id}
+                onClick={() => onSelect(chain.id)}
+                onContextMenu={(e) => handleContextMenu(e, chain)}
+                className={`p-3 cursor-pointer transition-colors ${
+                  selectedId === chain.id
+                    ? "bg-[rgba(117,170,252,0.15)]"
+                    : "hover:bg-[rgba(117,170,252,0.05)]"
+                }`}
+              >
+                <div className="flex items-center justify-between mb-1">
+                  <span className="font-mono text-sm text-[#dbe7ff] truncate">
+                    {chain.name}
+                  </span>
+                  <span
+                    className={`flex items-center gap-1 px-1.5 py-0.5 font-mono text-[10px] uppercase rounded ${getStatusColor(
+                      chain.status
+                    )}`}
+                  >
+                    {getStatusIcon(chain.status)}
+                    {chain.status}
+                  </span>
+                </div>
+                {chain.description && (
+                  <p className="font-mono text-xs text-[#8b949e] truncate mb-1">
+                    {chain.description}
+                  </p>
+                )}
+                <div className="flex items-center gap-3 font-mono text-[10px] text-[#556677]">
+                  <span>{chain.contractCount} contracts</span>
+                  <span>
+                    {new Date(chain.updatedAt).toLocaleDateString()}
+                  </span>
+                </div>
+              </div>
+            ))}
+          </div>
+        )}
+      </div>
+
+      {/* Context menu */}
+      {contextMenu && (
+        <div
+          className="fixed z-50 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] shadow-lg py-1"
+          style={{ top: contextMenu.y, left: contextMenu.x }}
+        >
+          <button
+            onClick={() => {
+              onSelect(contextMenu.chain.id);
+              setContextMenu(null);
+            }}
+            className="w-full px-4 py-2 text-left font-mono text-xs text-[#dbe7ff] hover:bg-[rgba(117,170,252,0.1)]"
+          >
+            View Details
+          </button>
+          {contextMenu.chain.status !== "archived" && (
+            <button
+              onClick={handleArchive}
+              className="w-full px-4 py-2 text-left font-mono text-xs text-red-400 hover:bg-red-400/10"
+            >
+              Archive
+            </button>
+          )}
+        </div>
+      )}
+    </div>
+  );
+}
-- 
cgit v1.2.3

