RefSelector.tsx

  1import { useState } from 'react'
  2import { GitBranch, Tag, Check, ChevronsUpDown } from 'lucide-react'
  3import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
  4import { Button } from '@/components/ui/button'
  5import { Input } from '@/components/ui/input'
  6import type { GitRef } from '@/lib/gitApi'
  7import { cn } from '@/lib/utils'
  8
  9interface RefSelectorProps {
 10  refs: GitRef[]
 11  currentRef: string
 12  onSelect: (ref: GitRef) => void
 13}
 14
 15// Branch / tag selector dropdown for the code browser. Shown in two groups
 16// (branches, tags) with an inline search filter.
 17export function RefSelector({ refs, currentRef, onSelect }: RefSelectorProps) {
 18  const [open, setOpen] = useState(false)
 19  const [filter, setFilter] = useState('')
 20
 21  const filtered = refs.filter((r) =>
 22    r.shortName.toLowerCase().includes(filter.toLowerCase()),
 23  )
 24  const branches = filtered.filter((r) => r.type === 'branch')
 25  const tags = filtered.filter((r) => r.type === 'tag')
 26
 27  return (
 28    <Popover open={open} onOpenChange={setOpen}>
 29      <PopoverTrigger asChild>
 30        <Button variant="outline" size="sm" className="gap-2 font-mono text-xs">
 31          <GitBranch className="size-3.5" />
 32          {currentRef}
 33          <ChevronsUpDown className="size-3 text-muted-foreground" />
 34        </Button>
 35      </PopoverTrigger>
 36      <PopoverContent align="start" className="w-64 p-2">
 37        <p className="mb-2 px-1 text-xs font-semibold text-muted-foreground">Switch branch / tag</p>
 38        <Input
 39          placeholder="Filter…"
 40          className="mb-2 h-7 text-xs"
 41          value={filter}
 42          onChange={(e) => setFilter(e.target.value)}
 43          autoFocus
 44        />
 45        <div className="max-h-64 overflow-y-auto">
 46          {branches.length > 0 && (
 47            <div className="mb-1">
 48              <p className="px-2 py-1 text-xs text-muted-foreground">Branches</p>
 49              {branches.map((ref) => (
 50                <RefItem
 51                  key={ref.name}
 52                  ref_={ref}
 53                  active={ref.shortName === currentRef}
 54                  onSelect={() => { onSelect(ref); setOpen(false); setFilter('') }}
 55                />
 56              ))}
 57            </div>
 58          )}
 59          {tags.length > 0 && (
 60            <div>
 61              <p className="px-2 py-1 text-xs text-muted-foreground">Tags</p>
 62              {tags.map((ref) => (
 63                <RefItem
 64                  key={ref.name}
 65                  ref_={ref}
 66                  active={ref.shortName === currentRef}
 67                  onSelect={() => { onSelect(ref); setOpen(false); setFilter('') }}
 68                />
 69              ))}
 70            </div>
 71          )}
 72          {filtered.length === 0 && (
 73            <p className="px-2 py-2 text-xs text-muted-foreground">No results</p>
 74          )}
 75        </div>
 76      </PopoverContent>
 77    </Popover>
 78  )
 79}
 80
 81function RefItem({
 82  ref_,
 83  active,
 84  onSelect,
 85}: {
 86  ref_: GitRef
 87  active: boolean
 88  onSelect: () => void
 89}) {
 90  return (
 91    <button
 92      onClick={onSelect}
 93      className={cn(
 94        'flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs hover:bg-muted',
 95        active && 'font-medium',
 96      )}
 97    >
 98      {ref_.type === 'branch' ? (
 99        <GitBranch className="size-3 shrink-0 text-muted-foreground" />
100      ) : (
101        <Tag className="size-3 shrink-0 text-muted-foreground" />
102      )}
103      <span className="flex-1 truncate font-mono">{ref_.shortName}</span>
104      {active && <Check className="size-3 text-muted-foreground" />}
105    </button>
106  )
107}