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 (
{emoji}
);
}
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}
>
);
}