SPB Git

Repository Public

bottom-bar.tsx 3.3 KB 66 lines tsx

1'use client';2import Link from 'next/link';3import { usePathname, useRouter } from 'next/navigation';4import { ListChecks, Plus, X } from 'lucide-react';5import { useSessions, useTasks } from '@/lib/queries';6import { useTerminals } from '@/stores/terminals';7import { useUi } from '@/stores/ui';8import { closeTab } from '@/lib/session-actions';9import { TASK_ACTIVE } from '@/lib/states';10import { SessionDot } from '@/components/sessions/state-badge';11import { IconButton } from '@/components/ui/button';12import { cn } from '@/lib/cn';13import { ConnectionIndicator } from './connection-indicator';14import { NotificationsBell } from './notifications';1516export function BottomBar() {17  const pathname = usePathname();18  const router = useRouter();19  const { openIds, activeId, activate } = useTerminals();20  const sessions = useSessions().data ?? [];21  const tasks = useTasks().data ?? [];22  const running = tasks.filter((t) => TASK_ACTIVE.includes(t.state)).length;23  const onTerminal = pathname.startsWith('/terminal');24  return (25    <footer className="flex h-11 shrink-0 items-center border-t border-line bg-bg">26      <div role="tablist" aria-label="Open terminals" className="no-scrollbar flex min-w-0 flex-1 items-center gap-1 overflow-x-auto px-2" data-testid="terminal-tabs">27        {openIds.map((id) => {28          const s = sessions.find((x) => x.id === id);29          const active = id === activeId && onTerminal;30          return (31            <div key={id} className={cn('group flex h-8 max-w-56 shrink-0 items-center rounded-full transition-colors', active ? 'bg-surface shadow-card' : 'hover:bg-hover')}>32              <button33                role="tab"34                aria-selected={active}35                data-testid="terminal-tab"36                data-session-id={id}37                onClick={() => {38                  activate(id);39                  router.push(`/terminal/?s=${id}`);40                }}41                className={cn('flex h-full min-w-0 items-center gap-2 pl-3 pr-1.5 text-xs font-medium', active ? 'text-text' : 'text-text-2')}42              >43                {s ? <SessionDot state={s.state} /> : <span className="size-2" />}44                <span className="truncate">{s?.title ?? 'Session'}</span>45              </button>46              <button aria-label={`Close tab ${s?.title ?? ''}`} onClick={() => closeTab(id)} className="mr-1.5 inline-flex size-5 items-center justify-center rounded-full text-muted opacity-0 hover:bg-hover hover:text-text focus-visible:opacity-100 group-hover:opacity-100">47                <X className="size-3" />48              </button>49            </div>50          );51        })}52        <IconButton label="New session" size="sm" className="mx-1 self-center" onClick={() => useUi.getState().openNewSession()}>53          <Plus className="size-3.5" />54        </IconButton>55      </div>56      <div className="flex shrink-0 items-center gap-1 px-3">57        <Link href="/tasks/" className={cn('tnum flex h-7 items-center gap-1.5 rounded-sm px-2 text-[11px] hover:bg-surface-2', running ? 'text-accent' : 'text-muted')} data-testid="running-tasks">58          <ListChecks className="size-3.5" aria-hidden /> {running} running {running === 1 ? 'task' : 'tasks'}59        </Link>60        <NotificationsBell className="size-7" />61        <ConnectionIndicator className="px-1.5" />62      </div>63    </footer>64  );65}66
Files main
Menu

Theme