import type { ProjectIconColor } from "@t3tools/contracts"; import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell"; import { getProjectFaviconResourceKey, isProjectFaviconFallbackUrl, } from "@t3tools/shared/projectFavicon"; import { BotIcon, BookOpenIcon, BracesIcon, CircuitBoardIcon, CloudCogIcon, Code2Icon, DatabaseIcon, FlaskConicalIcon, FolderCodeIcon, Gamepad2Icon, Globe2Icon, ImageIcon, Layers3Icon, MonitorIcon, MusicIcon, PackageIcon, ServerIcon, ShieldCheckIcon, ShoppingBagIcon, SmartphoneIcon, TerminalIcon, VideoIcon, } from "lucide-react"; import type { IconName } from "lucide-react/dynamic"; import type { ComponentType } from "react"; import { lazy, Suspense, useState } from "react"; import { useAtomValue } from "@effect/atom-react"; import { projectFaviconUrlAtom } from "../state/assets"; import { selectProjectIcon, type ProjectIconName } from "../projectIconModel"; import { projectIconColorClassName } from "../projectIconColors"; import { cn } from "~/lib/utils"; const DynamicIcon = lazy(() => import("lucide-react/dynamic").then((module) => ({ default: module.DynamicIcon })), ); function DynamicProjectIconFallback() { return ; } const PROJECT_ICONS: Record> = { ai: BotIcon, book: BookOpenIcon, braces: BracesIcon, circuit: CircuitBoardIcon, cloud: CloudCogIcon, code: Code2Icon, database: DatabaseIcon, desktop: MonitorIcon, "folder-code": FolderCodeIcon, game: Gamepad2Icon, image: ImageIcon, layers: Layers3Icon, mobile: SmartphoneIcon, music: MusicIcon, package: PackageIcon, security: ShieldCheckIcon, server: ServerIcon, shopping: ShoppingBagIcon, terminal: TerminalIcon, test: FlaskConicalIcon, video: VideoIcon, web: Globe2Icon, }; const PROJECT_ICON_COLOR_BY_NAME: Record = { ai: "violet", book: "amber", braces: "purple", circuit: "teal", cloud: "sky", code: "blue", database: "cyan", desktop: "indigo", "folder-code": "orange", game: "emerald", image: "pink", layers: "fuchsia", mobile: "lime", music: "fuchsia", package: "orange", security: "teal", server: "blue", shopping: "rose", terminal: "green", test: "yellow", video: "red", web: "sky", }; // The slice of a project that decides its icon. Every surface must pass the // project record itself (or a snapshot spread from it) so the saved title, favicon // and icon override always travel together. Passing a display label as the title // changes the automatic icon, which is how the command palette drifted once. export type ProjectFaviconProject = Pick< EnvironmentProject, "environmentId" | "workspaceRoot" | "title" | "faviconPath" | "projectIcon" >; export function ProjectFavicon(input: { project: ProjectFaviconProject; className?: string | undefined; fallbackIcon?: ComponentType<{ className?: string }>; }) { const { project } = input; const src = useAtomValue( projectFaviconUrlAtom({ environmentId: project.environmentId, cwd: project.workspaceRoot, faviconPath: project.faviconPath, }), ); if (project.projectIcon?.kind === "emoji") { return ; } if (project.projectIcon?.kind === "lucide") { const colorClassName = projectIconColorClassName(project.projectIcon.color); const iconClassName = cn( "inline-flex size-3.5 shrink-0 items-center justify-center", colorClassName, input.className, ); return ( ); } const automaticIconName = input.fallbackIcon ? null : selectProjectIcon(project.title, project.workspaceRoot); const FallbackIcon = input.fallbackIcon ?? (automaticIconName?.kind === "lucide" ? PROJECT_ICONS[automaticIconName.icon] : undefined); const fallbackEmoji = automaticIconName?.kind === "emoji" ? automaticIconName.emoji : undefined; const fallbackColorClassName = automaticIconName?.kind === "lucide" ? projectIconColorClassName(PROJECT_ICON_COLOR_BY_NAME[automaticIconName.icon]) : undefined; if (!src || isProjectFaviconFallbackUrl(src)) { return ( ); } const cacheKey = getProjectFaviconResourceKey( project.environmentId, project.workspaceRoot, project.faviconPath, ); return ( ); } function ProjectFaviconFallback({ className, colorClassName, icon: Icon, emoji, }: { readonly className?: string | undefined; readonly colorClassName?: string | undefined; readonly icon?: ComponentType<{ className?: string }> | undefined; readonly emoji?: string | undefined; }) { if (emoji) { return ( ); } if (!Icon) return null; return ; } function ProjectFaviconImage({ src, className, fallbackIcon: FallbackIcon, fallbackEmoji, fallbackColorClassName, }: { readonly src: string; readonly className?: string | undefined; readonly fallbackIcon?: ComponentType<{ className?: string }> | undefined; readonly fallbackEmoji?: string | undefined; readonly fallbackColorClassName?: string | undefined; }) { const [displayedSrc, setDisplayedSrc] = useState(() => src.startsWith("data:image/") ? src : null, ); const isLoading = displayedSrc !== src; const handleLoadError = (failedSrc: string) => { setDisplayedSrc((currentSrc) => (currentSrc === failedSrc ? null : currentSrc)); }; return ( <> {displayedSrc === null ? ( ) : null} {displayedSrc ? ( handleLoadError(displayedSrc)} /> ) : null} {isLoading ? ( { setDisplayedSrc(src); }} onError={() => handleLoadError(src)} /> ) : null} ); }