web/app/components/file-icon.tsxblame
View source
2fa7f2c1import iconMap from "@/lib/file-icon-map.json";
b1ace982import { CanopyLogo } from "@/app/components/canopy-logo";
2fa7f2c3
2fa7f2c4interface FileIconProps {
2fa7f2c5 type: "tree" | "file";
2fa7f2c6 name?: string;
2fa7f2c7 className?: string;
2fa7f2c8 size?: number;
2fa7f2c9}
2fa7f2c10
2fa7f2c11function getIconName(type: "tree" | "file", name?: string): string {
e33d23712 if (type === "tree") return "folder";
2fa7f2c13
2fa7f2c14 if (!name) return "file";
2fa7f2c15 const lower = name.toLowerCase();
2fa7f2c16
2fa7f2c17 // Check exact filename first
2fa7f2c18 const byName = (iconMap as any).fileNames[lower];
2fa7f2c19 if (byName) return byName;
2fa7f2c20
2fa7f2c21 // Check compound extensions (e.g. "test.ts", "config.js")
2fa7f2c22 const parts = lower.split(".");
2fa7f2c23 for (let i = 1; i < parts.length; i++) {
2fa7f2c24 const compoundExt = parts.slice(i).join(".");
2fa7f2c25 const byCompound = (iconMap as any).fileExtensions[compoundExt];
2fa7f2c26 if (byCompound) return byCompound;
2fa7f2c27 }
2fa7f2c28
2fa7f2c29 // Check single extension
2fa7f2c30 const ext = parts.pop() ?? "";
2fa7f2c31 const byExt = (iconMap as any).fileExtensions[ext];
2fa7f2c32 if (byExt) return byExt;
2fa7f2c33
2fa7f2c34 return "file";
2fa7f2c35}
2fa7f2c36
e33d23737function FolderIcon({ size }: { size: number }) {
e33d23738 return (
e33d23739 <svg
e33d23740 width={size}
e33d23741 height={size}
e33d23742 viewBox="0 0 16 16"
e33d23743 fill="none"
e33d23744 style={{ flexShrink: 0 }}
e33d23745 >
e33d23746 <path
e33d23747 d="M6.922 3.768l-.644-.536A1 1 0 0 0 5.638 3H2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H7.562a1 1 0 0 1-.64-.232z"
e33d23748 stroke="var(--accent)"
e33d23749 strokeWidth="0.9"
e33d23750 fill="none"
e33d23751 />
e33d23752 </svg>
e33d23753 );
e33d23754}
e33d23755
b1ace9856function CanopyFolderIcon({ size }: { size: number }) {
b1ace9857 const canopySize = Math.max(8, Math.round(size * 0.58));
b1ace9858 return (
b1ace9859 <span
b1ace9860 style={{
b1ace9861 width: size,
b1ace9862 height: size,
b1ace9863 position: "relative",
b1ace9864 display: "inline-flex",
b1ace9865 alignItems: "center",
b1ace9866 justifyContent: "center",
b1ace9867 flexShrink: 0,
b1ace9868 }}
b1ace9869 >
b1ace9870 <FolderIcon size={size} />
b1ace9871 <span
b1ace9872 style={{
b1ace9873 position: "absolute",
b1ace9874 right: 0,
b1ace9875 bottom: 0,
b1ace9876 lineHeight: 0,
b1ace9877 }}
b1ace9878 >
b1ace9879 <CanopyLogo size={canopySize} />
b1ace9880 </span>
b1ace9881 </span>
b1ace9882 );
b1ace9883}
b1ace9884
2fa7f2c85export function FileIcon({ type, name, className = "", size = 16 }: FileIconProps) {
e33d23786 if (type === "tree") {
b1ace9887 const isCanopyDir = (name ?? "").toLowerCase() === ".canopy";
e33d23788 return (
e33d23789 <span className={className}>
b1ace9890 {isCanopyDir ? <CanopyFolderIcon size={size} /> : <FolderIcon size={size} />}
e33d23791 </span>
e33d23792 );
e33d23793 }
e33d23794
2fa7f2c95 const iconName = getIconName(type, name);
2fa7f2c96
2fa7f2c97 return (
b87819998 <span
2fa7f2c99 className={className}
b878199100 style={{
b878199101 display: "inline-block",
b878199102 width: size,
b878199103 height: size,
b878199104 flexShrink: 0,
b878199105 backgroundColor: "var(--text-faint)",
b878199106 maskImage: `url(/file-icons/${iconName}.svg)`,
b878199107 maskSize: "contain",
b878199108 maskRepeat: "no-repeat",
b878199109 maskPosition: "center",
b878199110 WebkitMaskImage: `url(/file-icons/${iconName}.svg)`,
b878199111 WebkitMaskSize: "contain",
b878199112 WebkitMaskRepeat: "no-repeat",
b878199113 WebkitMaskPosition: "center",
b878199114 }}
b878199115 role="img"
b878199116 aria-hidden="true"
2fa7f2c117 />
2fa7f2c118 );
2fa7f2c119}