PAP-70: project icon infra + ProjectTile renderer + picker (PAP-68 part 3)

Adds the project icon system mirroring the agent icon pattern:

- shared: PROJECT_ICON_NAMES + ProjectIconName (default "folder", curated
  Lucide set), exported from index; tighten project validator icon field
  from loose string to z.enum(PROJECT_ICON_NAMES).
- ui: project-icons.ts (PROJECT_ICONS map + getProjectIcon defaulting to
  folder) and ProjectIconPicker (Popover + search + grid), mirroring the
  agent equivalents.
- ProjectTile gains an optional `icon` prop, rendering getProjectIcon(icon)
  instead of a hard-coded Folder (default unchanged). Color behavior intact.
- Pass icon={project.icon ?? null} at all three tile sites: Projects list,
  SidebarProjects, and the detail header (threaded through ColorPicker).
- Tests: ProjectTile renders chosen icon / falls back to folder for unknown.
- Backfill required Project.icon on test/storybook fixtures left un-updated
  by part 1, restoring a green UI typecheck.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
scotttongandClaude Opus 4.8 committed 2026-06-05 15:18:54 -07:00
1 parent 744f9f7428
commit ee196badee
22 files changed
+266 -16

No files matched your search

+50
View File
@@ -124,6 +124,56 @@ export const AGENT_ICON_NAMES = [
] as const;
export type AgentIconName = (typeof AGENT_ICON_NAMES)[number];
/**
* Curated Lucide icon set for projects (PAP-68 part 3).
*
* The first entry, `"folder"`, is the default for any project without an
* explicit icon. The remaining entries reuse much of the agent icon set plus a
* handful of folder/structure icons that read well at small tile sizes.
*/
export const PROJECT_ICON_NAMES = [
"folder",
"rocket",
"code",
"terminal",
"database",
"globe",
"package",
"boxes",
"box",
"layers",
"briefcase",
"compass",
"target",
"flame",
"zap",
"star",
"bug",
"wrench",
"hammer",
"lightbulb",
"sparkles",
"shield",
"lock",
"search",
"cog",
"brain",
"cpu",
"git-branch",
"file-code",
"puzzle",
"gem",
"atom",
"heart",
"mail",
"message-square",
"crown",
"radar",
"telescope",
"hexagon",
] as const;
export type ProjectIconName = (typeof PROJECT_ICON_NAMES)[number];
export const ISSUE_STATUSES = [
"backlog",
"todo",
+2
View File
@@ -41,6 +41,7 @@ export {
WORKSPACE_BRANCH_ROUTINE_VARIABLE,
MODEL_PROFILE_KEYS,
AGENT_ICON_NAMES,
PROJECT_ICON_NAMES,
ISSUE_STATUSES,
INBOX_MINE_ISSUE_STATUSES,
INBOX_MINE_ISSUE_STATUS_FILTER,
@@ -169,6 +170,7 @@ export {
type AgentRole,
type ModelProfileKey,
type AgentIconName,
type ProjectIconName,
type IssueStatus,
type IssuePriority,
type IssueWorkMode,
+2 -4
View File
@@ -1,5 +1,5 @@
import { z } from "zod";
import { PROJECT_STATUSES } from "../constants.js";
import { PROJECT_STATUSES, PROJECT_ICON_NAMES } from "../constants.js";
import { envConfigSchema } from "./secret.js";
import { trustAuthorizationPolicySchema } from "./trust-policy.js";
@@ -107,9 +107,7 @@ const projectFields = {
leadAgentId: z.string().uuid().optional().nullable(),
targetDate: z.string().optional().nullable(),
color: z.string().optional().nullable(),
// TODO(PAP-68 part 3): tighten to `z.enum(PROJECT_ICON_NAMES)` once the icon
// name enum lands. Loosely typed as a string until then.
icon: z.string().optional().nullable(),
icon: z.enum(PROJECT_ICON_NAMES).optional().nullable(),
env: envConfigSchema.optional().nullable(),
executionWorkspacePolicy: projectExecutionWorkspacePolicySchema.optional().nullable(),
archivedAt: z.string().datetime().optional().nullable(),