"use client"; import { useRef, useState, useEffect } from "react"; import { GraphNode, PRIORITY_LABELS, PRIORITY_COLORS, getPrefixLabel } from "@/lib/types"; import { formatRelativeTime } from "@/lib/utils"; interface BeadTooltipProps { node: GraphNode; x: number; y: number; prefixColor: string; allNodes: GraphNode[]; theme?: string; } export function BeadTooltip({ node, x, y, prefixColor, allNodes, theme }: BeadTooltipProps) { const COLORS = theme === "dark" ? { bg: "#18181b", // zinc-900 border: "#3f3f46", // zinc-700 borderLight: "#27272a", // zinc-800 shadow: "rgba(0,0,0,0.3)", text: "#f4f4f5", // zinc-100 textMuted: "#a1a1aa", // zinc-400 textDim: "#71717a", // zinc-500 } : { bg: "#FFFFFF", border: "#E5E7EB", borderLight: "#F3F4F6", shadow: "rgba(0,0,0,0.08)", text: "#131316", textMuted: "#737680", textDim: "#7F818B", }; const ref = useRef(null); const [pos, setPos] = useState({ x: 0, y: 0 }); useEffect(() => { if (!ref.current) return; const tt = ref.current.getBoundingClientRect(); const pad = 14; // Prefer placing above and to the right of cursor let nx = x + pad; let ny = y - tt.height - pad; // Clamp to viewport edges if (nx + tt.width > window.innerWidth - 16) { nx = x - tt.width - pad; } if (nx < 16) nx = 16; if (ny < 16) { ny = y + pad + 8; } if (ny + tt.height > window.innerHeight - 16) { ny = window.innerHeight - tt.height - 16; } setPos({ x: nx, y: ny }); }, [x, y]); // Resolve blocker IDs to short titles const blockers = node.dependentIds .map((id) => { const blockerNode = allNodes.find((n) => n.id === id); return blockerNode ? { id, title: blockerNode.title } : { id, title: id }; }) .filter(Boolean); const priorityLabel = PRIORITY_LABELS[node.priority] || `P${node.priority}`; const priorityColor = PRIORITY_COLORS[node.priority] || "#a1a1aa"; const labelStyle: React.CSSProperties = { fontSize: 10, fontWeight: 600, letterSpacing: 1.5, textTransform: "uppercase", color: theme === "dark" ? "#71717a" : "#7F818B", marginBottom: 3, }; const valueStyle: React.CSSProperties = { fontSize: 12, color: theme === "dark" ? "#a1a1aa" : "#737680", lineHeight: 1.45, }; return (
{/* Accent bar */}
{/* Prefix + ID */}
{getPrefixLabel(node.prefix)} {node.id}
{/* Title */}
{node.title}
{/* Labels */} {node.labels.length > 0 && (
{node.labels.slice(0, 3).map((label) => ( {label} ))} {node.labels.length > 3 && ( +{node.labels.length - 3} more )}
)} {/* Metadata rows */}
{/* Estimate */} {node.estimatedMinutes != null && (
Estimate
⏱ {node.estimatedMinutes}m
)} {/* Created */}
Created
{formatRelativeTime(node.createdAt)}
{/* Owner */} {node.owner && (
Owner
{node.owner}
)} {/* Assignee */} {(node.assignee || node.createdBy) && (
{node.assignee && ( <>
Assignee
{node.assignee}
)} {node.createdBy && !node.assignee && ( <>
Created by
{node.createdBy}
)} {node.createdBy && node.assignee && node.createdBy !== node.assignee && ( <>
Created by
{node.createdBy}
)}
)} {/* Blocked by */}
Blocked by
{blockers.length === 0 ? (
None
) : (
{blockers.map((b) => (
{b.id} {b.title}
))}
)}
{/* Priority */}
Priority
{priorityLabel}
); }