import { useEffect, useRef, useState } from "react"; import { Activity, ChevronRight, CircleCheck, CircleX, Copy, EyeOff, Loader2, MoreVertical, Pencil, Star, Trash2, } from "lucide-react"; import type { Connection } from "../../lib/types"; import { useConnectionStore } from "../../stores/connectionStore"; import { buildConfigFromConnection } from "./ConnectionCard"; import { buildConnectionUrl } from "../../lib/connectionString"; import { useConnectionStatus } from "./useConnectionStatus"; interface ConnectionCardMenuProps { connection: Connection; onEdit: () => void; onDuplicate: () => void; onDelete: () => void; } const menuItemClass = "flex w-full items-center gap-2 px-3 py-2 text-left text-xs text-text hover:bg-surface transition-colors cursor-pointer"; /** * Kebab (⋮) actions menu for a connection card. Hosts the favorite toggle, * on-demand connection test (inline status), and a Manage submenu * (Edit… / Duplicate / Delete…). Closes on outside mousedown, Escape, and * after selecting an action. */ export function ConnectionCardMenu({ connection, onEdit, onDuplicate, onDelete, }: ConnectionCardMenuProps) { const [open, setOpen] = useState(false); const [manageOpen, setManageOpen] = useState(false); const menuRef = useRef(null); const { state, info, check } = useConnectionStatus( connection.id, (pw) => buildConfigFromConnection(connection, pw), ); useEffect(() => { if (!open) return; const handleMouseDown = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { setOpen(false); setManageOpen(false); } }; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { setOpen(false); setManageOpen(false); } }; // Capture phase so we fire before other stopPropagation handlers document.addEventListener("mousedown", handleMouseDown, true); document.addEventListener("keydown", handleKeyDown); return () => { document.removeEventListener("mousedown", handleMouseDown, true); document.removeEventListener("keydown", handleKeyDown); }; }, [open]); const toggleMenu = () => { setOpen((prev) => { const next = !prev; if (!next) setManageOpen(false); return next; }); }; const close = () => { setOpen(false); setManageOpen(false); }; const copyUrl = async (withPassword: boolean) => { let password: string | null = null; if (withPassword) { password = await useConnectionStore .getState() .getConnectionPassword(connection.id) .catch(() => null); } const url = buildConnectionUrl(connection, password); await navigator.clipboard.writeText(url).catch(() => {}); close(); }; const statusLabel = state === "checking" ? "Testing…" : state === "online" ? `Online${info ? ` · ${info}` : ""}` : state === "offline" ? info : "Test connection"; const statusIcon = state === "checking" ? ( ) : state === "online" ? ( ) : state === "offline" ? ( ) : ( ); return (
{open && (
e.stopPropagation()} className="absolute top-full right-0 mt-1 w-64 rounded-md border border-border bg-canvas shadow-lg z-20 py-1 text-xs" > {manageOpen && (
)}
)}
); }