feat: foldable PROJECTS section in sidebar with color support

- Add `color` (text) and `archivedAt` (timestamp) columns to projects table
- Add PROJECT_COLORS palette constant (10 colors) in shared package
- Add color/archivedAt to Project type interface and Zod validators
- Auto-assign next available color from palette on project creation
- New SidebarProjects component with:
  - Collapsible PROJECTS header above WORK section
  - Caret toggle visible on hover (left of header)
  - Always-visible plus button (right of header) opens NewProjectDialog
  - Lists non-archived projects with colored rounded squares
  - Active project highlighted based on URL match
- Remove Projects nav item from WORK section in sidebar

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-23 09:14:08 -06:00
parent 3392f4dbfa
commit a57d3427f7
10 changed files with 224 additions and 10 deletions

View File

@@ -1,7 +1,6 @@
import {
Inbox,
CircleDot,
Hexagon,
Target,
LayoutDashboard,
Bot,
@@ -9,7 +8,6 @@ import {
History,
Search,
SquarePen,
ShieldCheck,
BookOpen,
Paperclip,
} from "lucide-react";
@@ -17,6 +15,7 @@ import { useQuery } from "@tanstack/react-query";
import { CompanySwitcher } from "./CompanySwitcher";
import { SidebarSection } from "./SidebarSection";
import { SidebarNavItem } from "./SidebarNavItem";
import { SidebarProjects } from "./SidebarProjects";
import { useDialog } from "../context/DialogContext";
import { useCompany } from "../context/CompanyContext";
import { sidebarBadgesApi } from "../api/sidebarBadges";
@@ -82,20 +81,15 @@ export function Sidebar() {
/>
</div>
<SidebarProjects />
<SidebarSection label="Work">
<SidebarNavItem to="/issues" label="Issues" icon={CircleDot} />
<SidebarNavItem to="/projects" label="Projects" icon={Hexagon} />
<SidebarNavItem to="/goals" label="Goals" icon={Target} />
</SidebarSection>
<SidebarSection label="Company">
<SidebarNavItem to="/agents" label="Agents" icon={Bot} />
<SidebarNavItem
to="/approvals"
label="Approvals"
icon={ShieldCheck}
badge={sidebarBadges?.approvals}
/>
<SidebarNavItem to="/costs" label="Costs" icon={DollarSign} />
<SidebarNavItem to="/activity" label="Activity" icon={History} />
</SidebarSection>

View File

@@ -0,0 +1,97 @@
import { useState } from "react";
import { NavLink, useLocation } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { ChevronRight, Plus } from "lucide-react";
import { useCompany } from "../context/CompanyContext";
import { useDialog } from "../context/DialogContext";
import { useSidebar } from "../context/SidebarContext";
import { projectsApi } from "../api/projects";
import { queryKeys } from "../lib/queryKeys";
import { cn } from "../lib/utils";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import type { Project } from "@paperclip/shared";
export function SidebarProjects() {
const [open, setOpen] = useState(true);
const { selectedCompanyId } = useCompany();
const { openNewProject } = useDialog();
const { isMobile, setSidebarOpen } = useSidebar();
const location = useLocation();
const { data: projects } = useQuery({
queryKey: queryKeys.projects.list(selectedCompanyId!),
queryFn: () => projectsApi.list(selectedCompanyId!),
enabled: !!selectedCompanyId,
});
// Filter out archived projects
const visibleProjects = (projects ?? []).filter(
(p: Project) => !p.archivedAt
);
// Extract current projectId from URL
const projectMatch = location.pathname.match(/^\/projects\/([^/]+)/);
const activeProjectId = projectMatch?.[1] ?? null;
return (
<Collapsible open={open} onOpenChange={setOpen}>
<div className="group">
<div className="flex items-center px-3 py-1.5">
<CollapsibleTrigger className="flex items-center gap-1 flex-1 min-w-0">
<ChevronRight
className={cn(
"h-3 w-3 text-muted-foreground/60 transition-transform opacity-0 group-hover:opacity-100",
open && "rotate-90"
)}
/>
<span className="text-[10px] font-medium uppercase tracking-widest font-mono text-muted-foreground/60">
Projects
</span>
</CollapsibleTrigger>
<button
onClick={(e) => {
e.stopPropagation();
openNewProject();
}}
className="flex items-center justify-center h-4 w-4 rounded text-muted-foreground/60 hover:text-foreground hover:bg-accent/50 transition-colors"
aria-label="New project"
>
<Plus className="h-3 w-3" />
</button>
</div>
</div>
<CollapsibleContent>
<div className="flex flex-col gap-0.5 mt-0.5">
{visibleProjects.map((project: Project) => (
<NavLink
key={project.id}
to={`/projects/${project.id}`}
onClick={() => {
if (isMobile) setSidebarOpen(false);
}}
className={cn(
"flex items-center gap-2.5 px-3 py-1.5 text-[13px] font-medium transition-colors",
activeProjectId === project.id
? "bg-accent text-foreground"
: "text-foreground/80 hover:bg-accent/50 hover:text-foreground"
)}
>
<span
className="shrink-0 h-3.5 w-3.5 rounded-sm"
style={{
backgroundColor: project.color ?? "#6366f1",
}}
/>
<span className="flex-1 truncate">{project.name}</span>
</NavLink>
))}
</div>
</CollapsibleContent>
</Collapsible>
);
}