SPB Git

Repository Public

command-palette.tsx 5.9 KB 114 lines tsx

1'use client';2import { useEffect, useState } from 'react';3import { useRouter } from 'next/navigation';4import { Command } from 'cmdk';5import { useQuery } from '@tanstack/react-query';6import type { SearchHit } from '@cr/shared';7import { File, FolderGit2, LayoutPanelLeft, Plus, Search } from 'lucide-react';8import { api, asList, qs } from '@/lib/api';9import { useProjects, useSessions } from '@/lib/queries';10import { PRESETS, useUi } from '@/stores/ui';11import { useCurrentProject, sortSessions } from '@/hooks/use-current';12import { useOpenSession } from '@/hooks/use-nav';13import { SessionDot } from '@/components/sessions/state-badge';14import { NAV } from './nav';1516const item = 'flex min-h-9 cursor-default select-none items-center gap-2.5 rounded-sm px-2.5 text-[13px] text-text-2 data-[selected=true]:bg-hover data-[selected=true]:text-text pointer-coarse:min-h-11';17const group = '[&_[cmdk-group-heading]]:px-2.5 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-[11px] [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:tracking-wide [&_[cmdk-group-heading]]:text-muted';1819export function CommandPalette() {20  const open = useUi((s) => s.paletteOpen);21  const setUi = useUi((s) => s.set);22  const router = useRouter();23  const openSession = useOpenSession();24  const sessions = useSessions().data ?? [];25  const projects = useProjects().data ?? [];26  const project = useCurrentProject();27  const [query, setQuery] = useState('');28  const [debounced, setDebounced] = useState('');29  useEffect(() => {30    const t = setTimeout(() => setDebounced(query.trim()), 200);31    return () => clearTimeout(t);32  }, [query]);33  useEffect(() => {34    if (!open) setQuery('');35  }, [open]);36  const files = useQuery({37    queryKey: ['palette-files', project?.id, debounced],38    queryFn: async () => asList<SearchHit>(await api.get(`/projects/${project?.id}/search${qs({ q: debounced, mode: 'name' })}`)),39    enabled: open && !!project && debounced.length >= 2,40    staleTime: 10_000,41  });42  const run = (fn: () => void) => {43    setUi({ paletteOpen: false });44    fn();45  };4647  return (48    <Command.Dialog49      open={open}50      onOpenChange={(o) => setUi({ paletteOpen: o })}51      label="Command palette"52      overlayClassName="anim-fade fixed inset-0 z-[60] bg-scrim"53      contentClassName="anim-pop fixed left-1/2 top-[12dvh] z-[60] w-[min(600px,calc(100vw-24px))] -translate-x-1/2 overflow-hidden rounded-lg border border-line-2 bg-surface shadow-pop"54    >55      <div className="flex items-center gap-2.5 border-b border-line px-3.5">56        <Search className="size-4 shrink-0 text-muted" aria-hidden />57        <Command.Input value={query} onValueChange={setQuery} placeholder="Search sessions, projects, files, commands…" data-testid="palette-input" className="h-12 w-full bg-transparent text-sm text-text placeholder:text-muted focus:outline-none" />58      </div>59      <Command.List className="max-h-[min(60dvh,440px)] overflow-y-auto p-1.5">60        <Command.Empty className="px-3 py-8 text-center text-xs text-muted">No results</Command.Empty>61        <Command.Group heading="Actions" className={group}>62          <Command.Item className={item} onSelect={() => run(() => useUi.getState().openNewSession())}>63            <Plus className="size-4 text-muted" /> New session64          </Command.Item>65        </Command.Group>66        {sessions.length > 0 && (67          <Command.Group heading="Switch session" className={group}>68            {sortSessions(sessions).map((s) => (69              <Command.Item key={s.id} value={`session ${s.title} ${s.projectName ?? ''} ${s.id}`} className={item} onSelect={() => run(() => openSession(s.id))}>70                <SessionDot state={s.state} /> <span className="truncate">{s.title}</span>71                <span className="ml-auto shrink-0 text-[11px] text-muted">{s.projectName ?? 'No project'}</span>72              </Command.Item>73            ))}74          </Command.Group>75        )}76        {projects.length > 0 && (77          <Command.Group heading="Open project" className={group}>78            {projects.map((p) => (79              <Command.Item key={p.id} value={`project ${p.name} ${p.path}`} className={item} onSelect={() => run(() => router.push(`/files/?p=${p.id}`))}>80                <FolderGit2 className="size-4 text-muted" /> <span className="truncate">{p.name}</span>81                <span className="ml-auto shrink-0 font-mono text-[11px] text-muted">{p.rootLabel}/{p.path}</span>82              </Command.Item>83            ))}84          </Command.Group>85        )}86        {project && (files.data?.length ?? 0) > 0 && (87          <Command.Group heading={`Files in ${project.name}`} className={group}>88            {files.data?.slice(0, 20).map((f) => (89              <Command.Item key={f.path} value={`file ${f.path}`} className={item} onSelect={() => run(() => router.push(`/files/?p=${project.id}&path=${encodeURIComponent(f.path)}`))}>90                <File className="size-4 text-muted" /> <span className="truncate font-mono text-xs">{f.path}</span>91              </Command.Item>92            ))}93          </Command.Group>94        )}95        <Command.Group heading="Go to" className={group}>96          {NAV.map((n) => (97            <Command.Item key={n.href} value={`go to ${n.label}`} className={item} onSelect={() => run(() => router.push(n.href))}>98              <n.icon className="size-4 text-muted" /> {n.label}99            </Command.Item>100          ))}101        </Command.Group>102        <Command.Group heading="Layout" className={group}>103          {PRESETS.map((p) => (104            <Command.Item key={p.id} value={`layout ${p.label}`} className={item} onSelect={() => run(() => { useUi.getState().applyPreset(p.id); router.push('/terminal/'); })}>105              <LayoutPanelLeft className="size-4 text-muted" /> {p.label}106              <span className="ml-auto text-[11px] text-muted">{p.hint}</span>107            </Command.Item>108          ))}109        </Command.Group>110      </Command.List>111    </Command.Dialog>112  );113}114
Files main
Menu

Theme