SPB Git

Repository Public

sidebar.tsx 5.3 KB 109 lines tsx

1'use client';2import Link from 'next/link';3import { usePathname } from 'next/navigation';4import { PanelLeftClose, PanelLeftOpen, Plus } from 'lucide-react';5import { useSessions } from '@/lib/queries';6import { useUi } from '@/stores/ui';7import { useTerminals } from '@/stores/terminals';8import { useNow } from '@/hooks/use-viewport';9import { useOpenSession } from '@/hooks/use-nav';10import { sortSessions } from '@/hooks/use-current';11import { IconButton } from '@/components/ui/button';12import { Tooltip } from '@/components/ui/tooltip';13import { SessionRow } from '@/components/sessions/session-row';14import { SessionDot } from '@/components/sessions/state-badge';15import { cn } from '@/lib/cn';16import { NAV } from './nav';17import { Logo } from './logo';18import { useBrand } from '@/stores/branding';1920const GROUPS: { label: string; items: string[] }[] = [21  { label: 'Work', items: ['/terminal/', '/projects/', '/sessions/', '/files/', '/tasks/'] },22  { label: 'Platform', items: ['/skills/', '/mcp/', '/cluster/', '/system/', '/settings/'] },23];2425export function Sidebar() {26  const brand = useBrand();27  const pathname = usePathname();28  const collapsed = useUi((s) => s.sidebarCollapsed);29  const setUi = useUi((s) => s.set);30  const openNew = useUi((s) => s.openNewSession);31  const sessions = useSessions().data;32  const activeId = useTerminals((s) => s.activeId);33  const open = useOpenSession();34  const now = useNow(15_000);35  const sorted = sortSessions(sessions ?? []);3637  return (38    <nav aria-label="Primary" data-collapsed={collapsed} className={cn('flex h-full shrink-0 flex-col border-r border-line bg-bg transition-[width] duration-150', collapsed ? 'w-14' : 'w-60')}>39      <div className={cn('flex h-14 items-center', collapsed ? 'justify-center' : 'gap-2.5 px-4')}>40        <Logo className="size-7 shrink-0 rounded-[9px]" />41        {!collapsed && <span className="flex-1 truncate text-[14px] font-bold tracking-tight" data-testid="wordmark">{brand.product}</span>}42        {!collapsed && (43          <IconButton label="Collapse sidebar" size="sm" onClick={() => setUi({ sidebarCollapsed: true })}>44            <PanelLeftClose className="size-4" />45          </IconButton>46        )}47      </div>48      {GROUPS.map((g) => (49        <div key={g.label} className={cn('pt-3', collapsed ? 'px-0' : 'px-2.5')}>50          {!collapsed && <p className="mb-1 px-2.5 text-[11px] font-semibold uppercase tracking-[0.06em] text-faint">{g.label}</p>}51          <ul className={cn('flex flex-col gap-0.5', collapsed && 'items-center')}>52            {NAV.filter((n) => g.items.includes(n.href)).map((n) => {53              const active = pathname === n.href || pathname === n.href.slice(0, -1);54              const link = (55                <Link56                  href={n.href}57                  aria-current={active ? 'page' : undefined}58                  aria-label={collapsed ? n.label : undefined}59                  className={cn(60                    'flex h-[34px] items-center gap-2.5 rounded-[10px] text-[13px] font-medium transition-colors',61                    collapsed ? 'w-9 justify-center' : 'px-2.5',62                    active ? 'bg-surface text-text shadow-card' : 'text-text-2 hover:bg-hover hover:text-text',63                  )}64                >65                  <n.icon className={cn('size-4 shrink-0', active ? 'text-accent' : 'text-muted')} strokeWidth={active ? 2.3 : 2} aria-hidden />66                  {!collapsed && n.label}67                </Link>68              );69              return <li key={n.href}>{collapsed ? <Tooltip content={n.label} side="right">{link}</Tooltip> : link}</li>;70            })}71          </ul>72        </div>73      ))}74      <div className="mx-4 mt-3 h-px bg-line" />75      <div className={cn('flex min-h-0 flex-1 flex-col py-3', collapsed ? 'items-center' : 'px-2.5')}>76        {!collapsed && (77          <div className="mb-1 flex h-6 items-center px-2.5">78            <span className="flex-1 text-[11px] font-semibold uppercase tracking-[0.06em] text-faint">Sessions</span>79            <IconButton label="New session" size="sm" onClick={() => openNew()}>80              <Plus className="size-3.5" />81            </IconButton>82          </div>83        )}84        <div className="no-scrollbar min-h-0 flex-1 overflow-y-auto" data-testid="sidebar-sessions">85          {sessions && sorted.length === 0 && !collapsed && <p className="px-2 py-2 text-xs text-muted">No sessions yet</p>}86          {sorted.map((s) =>87            collapsed ? (88              <Tooltip key={s.id} content={s.title} side="right">89                <button aria-label={`Open session ${s.title}`} onClick={() => open(s.id)} className={cn('flex size-8 items-center justify-center rounded-sm hover:bg-surface-2', s.id === activeId && 'bg-surface-2')}>90                  <SessionDot state={s.state} />91                </button>92              </Tooltip>93            ) : (94              <SessionRow key={s.id} session={s} dense active={s.id === activeId} onSelect={() => open(s.id)} now={now} />95            ),96          )}97        </div>98      </div>99      {collapsed && (100        <div className="flex justify-center border-t border-line py-2">101          <IconButton label="Expand sidebar" tooltipSide="right" onClick={() => setUi({ sidebarCollapsed: false })}>102            <PanelLeftOpen className="size-4" />103          </IconButton>104        </div>105      )}106    </nav>107  );108}109
Files main
Menu

Theme