164 lines
4.6 KiB
TypeScript
164 lines
4.6 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { ChevronRight, ChevronDown, Circle, CheckCircle2, Clock } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export interface TreeNode {
|
|
id: string;
|
|
label: string;
|
|
status?: "built" | "in_progress" | "missing";
|
|
children?: TreeNode[];
|
|
metadata?: {
|
|
sessionsCount?: number;
|
|
commitsCount?: number;
|
|
cost?: number;
|
|
};
|
|
}
|
|
|
|
interface TreeViewProps {
|
|
data: TreeNode[];
|
|
selectedId?: string | null;
|
|
onSelect?: (node: TreeNode) => void;
|
|
className?: string;
|
|
}
|
|
|
|
interface TreeNodeItemProps {
|
|
node: TreeNode;
|
|
level: number;
|
|
selectedId?: string | null;
|
|
onSelect?: (node: TreeNode) => void;
|
|
}
|
|
|
|
function TreeNodeItem({ node, level, selectedId, onSelect }: TreeNodeItemProps) {
|
|
const [isExpanded, setIsExpanded] = useState(level === 0); // Auto-expand top level
|
|
const hasChildren = node.children && node.children.length > 0;
|
|
const isSelected = selectedId === node.id;
|
|
|
|
const getStatusIcon = () => {
|
|
if (!node.status) return null;
|
|
|
|
switch (node.status) {
|
|
case "built":
|
|
return <CheckCircle2 className="h-3 w-3 text-green-600" />;
|
|
case "in_progress":
|
|
return <Clock className="h-3 w-3 text-blue-600" />;
|
|
case "missing":
|
|
return <Circle className="h-3 w-3 text-gray-400" />;
|
|
}
|
|
};
|
|
|
|
const getStatusColor = () => {
|
|
if (!node.status) return "";
|
|
|
|
switch (node.status) {
|
|
case "built":
|
|
return "bg-green-50 hover:bg-green-100 border-l-2 border-l-green-500";
|
|
case "in_progress":
|
|
return "bg-blue-50 hover:bg-blue-100 border-l-2 border-l-blue-500";
|
|
case "missing":
|
|
return "hover:bg-gray-100 border-l-2 border-l-transparent";
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<div
|
|
className={cn(
|
|
"flex items-center gap-1.5 py-1.5 px-2 rounded-md cursor-pointer transition-all group",
|
|
isSelected
|
|
? "bg-primary/10 text-primary font-medium"
|
|
: getStatusColor(),
|
|
level > 0 && "text-sm"
|
|
)}
|
|
style={{ paddingLeft: `${level * 12 + 8}px` }}
|
|
onClick={() => {
|
|
if (hasChildren) {
|
|
setIsExpanded(!isExpanded);
|
|
}
|
|
if (onSelect) {
|
|
onSelect(node);
|
|
}
|
|
}}
|
|
>
|
|
{hasChildren ? (
|
|
<button
|
|
className="hover:bg-muted/50 rounded p-0.5 transition-transform"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setIsExpanded(!isExpanded);
|
|
}}
|
|
>
|
|
{isExpanded ? (
|
|
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
|
|
) : (
|
|
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
|
|
)}
|
|
</button>
|
|
) : (
|
|
<div className="w-4 flex items-center justify-center">
|
|
{getStatusIcon()}
|
|
</div>
|
|
)}
|
|
|
|
<span className={cn(
|
|
"flex-1 truncate",
|
|
level === 0 && "font-semibold text-xs uppercase tracking-wide text-muted-foreground",
|
|
level === 1 && "font-medium",
|
|
level > 1 && "text-muted-foreground"
|
|
)}>
|
|
{node.label}
|
|
</span>
|
|
|
|
{node.metadata && (
|
|
<div className="flex items-center gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
{node.metadata.sessionsCount && node.metadata.sessionsCount > 0 && (
|
|
<span className="text-[10px] text-blue-600 font-medium bg-blue-100 px-1.5 py-0.5 rounded">
|
|
{node.metadata.sessionsCount}s
|
|
</span>
|
|
)}
|
|
{node.metadata.commitsCount && node.metadata.commitsCount > 0 && (
|
|
<span className="text-[10px] text-green-600 font-medium bg-green-100 px-1.5 py-0.5 rounded">
|
|
{node.metadata.commitsCount}c
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{hasChildren && isExpanded && (
|
|
<div className="space-y-0.5">
|
|
{node.children!.map((child) => (
|
|
<TreeNodeItem
|
|
key={child.id}
|
|
node={child}
|
|
level={level + 1}
|
|
selectedId={selectedId}
|
|
onSelect={onSelect}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function TreeView({ data, selectedId, onSelect, className }: TreeViewProps) {
|
|
return (
|
|
<div className={cn("space-y-0.5", className)}>
|
|
{data.map((node) => (
|
|
<TreeNodeItem
|
|
key={node.id}
|
|
node={node}
|
|
level={0}
|
|
selectedId={selectedId}
|
|
onSelect={onSelect}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
|
|
|