diff --git a/ui/src/pages/ProjectDetail.tsx b/ui/src/pages/ProjectDetail.tsx index 0a4972bba11..5ed0139b456 100644 --- a/ui/src/pages/ProjectDetail.tsx +++ b/ui/src/pages/ProjectDetail.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useState, useRef } from "react"; import { Link, useParams, useNavigate, useLocation, Navigate } from "@/lib/router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { PROJECT_COLORS, isUuidLike, type BudgetPolicySummary } from "@paperclipai/shared"; +import { PROJECT_COLORS, PROJECT_ICON_NAMES, isUuidLike, type BudgetPolicySummary } from "@paperclipai/shared"; import { budgetsApi } from "../api/budgets"; import { executionWorkspacesApi } from "../api/execution-workspaces"; import { instanceSettingsApi } from "../api/instanceSettings"; @@ -28,7 +28,11 @@ import { MembershipAction } from "../components/MembershipAction"; import { buildProjectWorkspaceSummaries } from "../lib/project-workspaces-tab"; import { collectLiveIssueIds } from "../lib/liveIssueIds"; import { projectRouteRef } from "../lib/utils"; +import { PROJECT_ICONS } from "../lib/project-icons"; import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; import { Tabs } from "@/components/ui/tabs"; import { PluginLauncherOutlet } from "@/plugins/launchers"; import { PluginSlotMount, PluginSlotOutlet, usePluginSlots } from "@/plugins/slots"; @@ -104,52 +108,88 @@ function OverviewContent({ ); } -/* ── Color picker popover ── */ +/* ── Combined icon + color picker popover (PAP-72 / PAP-68 part 4) ── */ -function ColorPicker({ +const DEFAULT_PROJECT_ICON = "folder"; + +function ProjectTilePicker({ color, icon, - onSelect, + onSelectIcon, + onSelectColor, }: { color: string | null; - icon?: string | null; - onSelect: (color: string | null) => void; + icon: string | null; + onSelectIcon: (icon: string) => void; + onSelectColor: (color: string | null) => void; }) { const [open, setOpen] = useState(false); - const ref = useRef(null); + const [search, setSearch] = useState(""); - useEffect(() => { - if (!open) return; - function handleClick(e: MouseEvent) { - if (ref.current && !ref.current.contains(e.target as Node)) { - setOpen(false); - } - } - document.addEventListener("mousedown", handleClick); - return () => document.removeEventListener("mousedown", handleClick); - }, [open]); + const filteredIcons = useMemo(() => { + const entries = PROJECT_ICON_NAMES.map((name) => [name, PROJECT_ICONS[name]] as const); + if (!search) return entries; + const q = search.toLowerCase(); + return entries.filter(([name]) => name.includes(q)); + }, [search]); + // Keep the popover open across selections so the user can pick both an icon + // and a color in one pass; reset the search when it closes. return ( -
- - {open && ( -
-

Project color

+ { + setOpen(next); + if (!next) setSearch(""); + }} + > + + + + + {/* Icon search + grid */} +

Icon

+ setSearch(e.target.value)} + className="mb-2 h-8 text-sm" + autoFocus + /> +
+ {filteredIcons.map(([name, Icon]) => ( + + ))} + {filteredIcons.length === 0 && ( +

No icons match

+ )} +
+ + {/* Color swatches */} +
+

Color

- {/* Neutral / reset-to-gray option (folder icon is fixed this phase) */} + {/* Neutral / reset-to-gray option */}
- )} -
+ + ); } @@ -722,10 +759,11 @@ export function ProjectDetail() { ) : null}
- updateProject.mutate({ color })} + onSelectIcon={(icon) => updateProject.mutate({ icon })} + onSelectColor={(color) => updateProject.mutate({ color })} />