VIBN Frontend for Coolify deployment

This commit is contained in:
2026-02-15 19:25:52 -08:00
commit 40bf8428cd
398 changed files with 76513 additions and 0 deletions

163
components/ui/tree-view.tsx Normal file
View File

@@ -0,0 +1,163 @@
"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>
);
}