spool-memory 0.1.1

Local-first developer memory system — persistent, structured knowledge for AI coding tools
Documentation
import { Inbox, MessageSquare, Compass, ShieldCheck, type LucideIcon } from 'lucide-react'
import { useI18n } from '@/state/i18n'
import { ProjectSwitcher } from '@/components/project-switcher'

export type SidebarView = 'inbox' | 'sessions' | 'index' | 'lint'

interface SidebarItemProps {
  icon: LucideIcon
  label: string
  active: boolean
  onClick: () => void
}

function SidebarItem({ icon: Icon, label, active, onClick }: SidebarItemProps) {
  return (
    <button
      type="button"
      onClick={onClick}
      title={label}
      className={
        'group relative flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left text-xs transition-all duration-150 ease-out-expo cursor-pointer ' +
        (active
          ? 'bg-bg-elevated text-foreground font-medium shadow-sm'
          : 'text-muted-foreground hover:bg-bg-elevated/60 hover:text-foreground')
      }
    >
      {/* Active indicator bar */}
      <div
        className={`absolute left-0 top-2 bottom-2 w-0.5 rounded-r transition-all duration-200 ease-out-expo ${
          active ? 'bg-primary opacity-100' : 'bg-primary opacity-0 group-hover:opacity-30'
        }`}
      />
      <Icon className={`h-4 w-4 shrink-0 transition-colors duration-150 ${active ? 'text-primary' : ''}`} />
      <span className="truncate">{label}</span>
    </button>
  )
}

export function AppSidebar({
  view,
  onChange,
}: {
  view: SidebarView
  onChange: (v: SidebarView) => void
}) {
  const { t } = useI18n()

  return (
    <aside className="flex w-48 flex-col border-r border-border-subtle bg-bg-deep p-2.5">
      <div className="mb-3">
        <ProjectSwitcher />
      </div>
      <nav className="flex flex-col gap-0.5">
        <SidebarItem
          icon={Inbox}
          label={t('nav.inbox')}
          active={view === 'inbox'}
          onClick={() => onChange('inbox')}
        />
        <SidebarItem
          icon={MessageSquare}
          label={t('nav.sessions')}
          active={view === 'sessions'}
          onClick={() => onChange('sessions')}
        />
        <SidebarItem
          icon={Compass}
          label={t('nav.index')}
          active={view === 'index'}
          onClick={() => onChange('index')}
        />
        <SidebarItem
          icon={ShieldCheck}
          label={t('nav.lint')}
          active={view === 'lint'}
          onClick={() => onChange('lint')}
        />
      </nav>
    </aside>
  )
}