import { useNavigate } from "@tanstack/react-router"; import { useEffect, useId, useRef, useState } from "react"; import { useDismiss } from "~/hooks/use-dismiss"; import { fuzzyFilter } from "~/lib/fuzzy"; import { cn } from "~/utils/cn"; const MAX_RESULTS = 60; const SHORT_SHA = 12; type Kind = "branches" | "tags"; type Props = { owner: string; repo: string; current: string; branches: string[]; tags: string[]; path: string; mode: "tree" | "blob" | "commits" | "blame"; }; /** * Branch and tag picker: a styled trigger that opens a fuzzy-filterable list, with a branches/tags * toggle so hundreds of tags never crowd the branch list. Keyboard: type to filter, arrows, Enter, Escape. */ export function RefSwitcher({ owner, repo, current, branches, tags, path, mode }: Props) { const navigate = useNavigate(); const [open, setOpen] = useState(false); const [kind, setKind] = useState(tags.includes(current) && !branches.includes(current) ? "tags" : "branches"); const [query, setQuery] = useState(""); const [active, setActive] = useState(0); const inputRef = useRef(null); const listId = useId(); const ref = useDismiss(open, () => setOpen(false)); const refs = kind === "branches" ? branches : tags; const results = fuzzyFilter(query, refs, MAX_RESULTS).map((match) => match.path); const known = branches.includes(current) || tags.includes(current); useEffect(() => { if (open) { setQuery(""); setActive(0); inputRef.current?.focus(); } }, [open]); useEffect(() => { setActive(0); }, [query, kind]); function go(target: string) { setOpen(false); const splat = path ? `${target}/${path}` : target; if (mode === "commits") navigate({ to: "/$owner/$repo/commits/$", params: { owner, repo, _splat: splat } }); else if (mode === "blame") navigate({ to: "/$owner/$repo/blame/$", params: { owner, repo, _splat: splat } }); else if (mode === "blob") navigate({ to: "/$owner/$repo/blob/$", params: { owner, repo, _splat: splat } }); else navigate({ to: "/$owner/$repo/tree/$", params: { owner, repo, _splat: splat } }); } function onKeyDown(event: React.KeyboardEvent) { if (event.key === "ArrowDown") { event.preventDefault(); setActive((index) => Math.min(index + 1, results.length - 1)); } else if (event.key === "ArrowUp") { event.preventDefault(); setActive((index) => Math.max(index - 1, 0)); } else if (event.key === "Enter") { event.preventDefault(); const target = results[active]; if (target) go(target); } else if (event.key === "Tab" && !event.shiftKey && results.length > 0) { event.preventDefault(); setKind((value) => (value === "branches" ? "tags" : "branches")); } } return (
{open && (
setQuery(event.target.value)} onKeyDown={onKeyDown} placeholder={kind === "branches" ? "Find a branch…" : "Find a tag…"} className="h-8 w-full rounded-md border border-gray-700 bg-gray-900 px-2 font-mono text-xs text-gray-100 placeholder:text-gray-600 focus:border-accent-500 focus:outline-none" />
{(["branches", "tags"] as const).map((item) => ( ))}
    {results.length === 0 && (
  • {refs.length === 0 ? `No ${kind}` : `Nothing matches “${query}”`}
  • )} {results.map((item, index) => (
  • ))}
)}
); }