SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
4.8 KB · 97 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import Link from "next/link";4import { usePathname } from "next/navigation";5import { ArrowLeftRight, FolderKanban, Plus } from "lucide-react";6import { useApp } from "@/components/app/store";7import { useApi } from "@/lib/client/api";8import { Skeleton } from "@/components/ui/misc";9import { Tooltip } from "@/components/ui/tooltip";10import type { PublicProject } from "@/lib/client/types";11import { ProjectIcon } from "./project-icon";12import { ProjectSwitcher } from "./project-switcher";13import { cn } from "@/lib/utils";1415const MAX_ROWS = 8;1617/**18 * Projects list rendered inside the sidebar / mobile drawer.19 * - rows link to the project page; the current page's project is highlighted20 * - the *active* project (used for new chats) shows a coloured dot + "Active" hint21 * - the ⇄ button opens the quick switcher (ActionSheet on phones, dropdown on desktop)22 */23export function ProjectsSidebarSection({ onNavigate }: { onNavigate?: () => void }) {24  const { activeProjectId } = useApp();25  const pathname = usePathname();26  const { data, isLoading } = useApi<{ projects: PublicProject[] }>("/api/projects");27  const all = React.useMemo(() => (data?.projects ?? []).filter((p) => !p.archived), [data]);28  const list = React.useMemo(() => {29    // Keep the active project visible even when it is not among the first rows.30    const head = all.slice(0, MAX_ROWS);31    const active = activeProjectId ? all.find((p) => p.id === activeProjectId) : undefined;32    if (active && !head.some((p) => p.id === active.id)) return [active, ...head.slice(0, MAX_ROWS - 1)];33    return head;34  }, [all, activeProjectId]);35  const currentId = pathname.match(/^\/app\/projects\/([^/]+)/)?.[1];36  const overflow = all.length - list.length;3738  return (39    <div className="pt-2">40      <div className="flex items-center justify-between px-2 pb-0.5">41        <Link href="/app/projects" onClick={onNavigate} className="tap inline-flex items-center gap-1 text-[11px] font-medium uppercase tracking-wide text-fg-subtle hover:text-fg">42          <FolderKanban className="size-3" /> Projects43          {all.length ? <span className="ml-1 font-normal text-fg-subtle/70">{all.length}</span> : null}44        </Link>45        <div className="flex items-center gap-0.5">46          {all.length ? (47            <ProjectSwitcher onNavigate={onNavigate}>48              <button type="button" className={cn("tap rounded p-0.5 hover:text-fg", activeProjectId ? "text-accent" : "text-fg-subtle")} aria-label="Switch active project">49                <ArrowLeftRight className="size-3.5" />50              </button>51            </ProjectSwitcher>52          ) : null}53          <Tooltip content="New project">54            <Link href="/app/projects?new=1" onClick={onNavigate} className="tap rounded p-0.5 text-fg-subtle hover:text-fg" aria-label="New project">55              <Plus className="size-3.5" />56            </Link>57          </Tooltip>58        </div>59      </div>60      {isLoading && !data ? (61        <div className="space-y-1 px-2 pt-1">62          <Skeleton className="h-7" />63          <Skeleton className="h-7" />64        </div>65      ) : list.length === 0 ? (66        <Link href="/app/projects?new=1" onClick={onNavigate} className="mx-2 mt-1 block rounded-md border border-dashed border-border px-2.5 py-2 text-[12px] text-fg-subtle hover:border-border-strong hover:text-fg">67          Group chats, files and instructions into a project.68        </Link>69      ) : (70        <div className="space-y-px">71          {list.map((p) => {72            const isActive = p.id === activeProjectId;73            const isCurrent = p.id === currentId;74            return (75              <Link key={p.id} href={`/app/projects/${p.id}`} onClick={onNavigate} aria-current={isCurrent ? "page" : undefined} className={cn("group flex min-h-[36px] items-center gap-2 rounded-md px-2 text-[13px] md:min-h-[30px]", isCurrent ? "bg-bg-muted text-fg" : "text-fg-muted hover:bg-bg-muted/70 hover:text-fg")}>76                <ProjectIcon icon={p.icon} color={p.color} size="sm" className="size-5 text-[12px]" />77                <span className="min-w-0 flex-1 truncate">{p.name}</span>78                {isActive ? (79                  <span className="inline-flex items-center gap-1 text-[10px] font-medium uppercase tracking-wide text-accent" title="Active project for new chats">80                    <span className="size-1.5 rounded-full bg-accent" aria-hidden />81                    Active82                  </span>83                ) : null}84              </Link>85            );86          })}87          {overflow > 0 ? (88            <Link href="/app/projects" onClick={onNavigate} className="block rounded-md px-2 py-1 text-[12px] text-fg-subtle hover:bg-bg-muted/70 hover:text-fg">89              +{overflow} more…90            </Link>91          ) : null}92        </div>93      )}94    </div>95  );96}97