Files
vibn-frontend/components/ui/tree-view.tsx
Mark Henderson 74f8dc4282 fix(ui): make Justine palette visible on marketing + trim rainbow chrome
- Replace blue/purple gradients with ink gradient text and cream/parch CTA surface
- Step badges and transformation icons use primary (ink) fills
- /features page icons unified to text-primary; Lora section titles
- Tree view status colors use semantic tokens instead of blue/green

Made-with: Cursor
2026-04-01 21:09:18 -07:00

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-primary" />;
case "in_progress":
return <Clock className="h-3 w-3 text-muted-foreground" />;
case "missing":
return <Circle className="h-3 w-3 text-muted-foreground/50" />;
}
};
const getStatusColor = () => {
if (!node.status) return "";
switch (node.status) {
case "built":
return "bg-secondary hover:bg-muted border-l-2 border-l-primary";
case "in_progress":
return "bg-muted/40 hover:bg-muted border-l-2 border-l-border";
case "missing":
return "hover:bg-muted/30 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] font-medium text-primary bg-primary/10 px-1.5 py-0.5 rounded">
{node.metadata.sessionsCount}s
</span>
)}
{node.metadata.commitsCount && node.metadata.commitsCount > 0 && (
<span className="text-[10px] font-medium text-muted-foreground bg-muted 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>
);
}