| 2fa7f2c | | | 1 | import iconMap from "@/lib/file-icon-map.json"; |
| b1ace98 | | | 2 | import { CanopyLogo } from "@/app/components/canopy-logo"; |
| 2fa7f2c | | | 3 | |
| 2fa7f2c | | | 4 | interface FileIconProps { |
| 2fa7f2c | | | 5 | type: "tree" | "file"; |
| 2fa7f2c | | | 6 | name?: string; |
| 2fa7f2c | | | 7 | className?: string; |
| 2fa7f2c | | | 8 | size?: number; |
| 2fa7f2c | | | 9 | } |
| 2fa7f2c | | | 10 | |
| 2fa7f2c | | | 11 | function getIconName(type: "tree" | "file", name?: string): string { |
| e33d237 | | | 12 | if (type === "tree") return "folder"; |
| 2fa7f2c | | | 13 | |
| 2fa7f2c | | | 14 | if (!name) return "file"; |
| 2fa7f2c | | | 15 | const lower = name.toLowerCase(); |
| 2fa7f2c | | | 16 | |
| 2fa7f2c | | | 17 | // Check exact filename first |
| 2fa7f2c | | | 18 | const byName = (iconMap as any).fileNames[lower]; |
| 2fa7f2c | | | 19 | if (byName) return byName; |
| 2fa7f2c | | | 20 | |
| 2fa7f2c | | | 21 | // Check compound extensions (e.g. "test.ts", "config.js") |
| 2fa7f2c | | | 22 | const parts = lower.split("."); |
| 2fa7f2c | | | 23 | for (let i = 1; i < parts.length; i++) { |
| 2fa7f2c | | | 24 | const compoundExt = parts.slice(i).join("."); |
| 2fa7f2c | | | 25 | const byCompound = (iconMap as any).fileExtensions[compoundExt]; |
| 2fa7f2c | | | 26 | if (byCompound) return byCompound; |
| 2fa7f2c | | | 27 | } |
| 2fa7f2c | | | 28 | |
| 2fa7f2c | | | 29 | // Check single extension |
| 2fa7f2c | | | 30 | const ext = parts.pop() ?? ""; |
| 2fa7f2c | | | 31 | const byExt = (iconMap as any).fileExtensions[ext]; |
| 2fa7f2c | | | 32 | if (byExt) return byExt; |
| 2fa7f2c | | | 33 | |
| 2fa7f2c | | | 34 | return "file"; |
| 2fa7f2c | | | 35 | } |
| 2fa7f2c | | | 36 | |
| e33d237 | | | 37 | function FolderIcon({ size }: { size: number }) { |
| e33d237 | | | 38 | return ( |
| e33d237 | | | 39 | <svg |
| e33d237 | | | 40 | width={size} |
| e33d237 | | | 41 | height={size} |
| e33d237 | | | 42 | viewBox="0 0 16 16" |
| e33d237 | | | 43 | fill="none" |
| e33d237 | | | 44 | style={{ flexShrink: 0 }} |
| e33d237 | | | 45 | > |
| e33d237 | | | 46 | <path |
| e33d237 | | | 47 | 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" |
| e33d237 | | | 48 | stroke="var(--accent)" |
| e33d237 | | | 49 | strokeWidth="0.9" |
| e33d237 | | | 50 | fill="none" |
| e33d237 | | | 51 | /> |
| e33d237 | | | 52 | </svg> |
| e33d237 | | | 53 | ); |
| e33d237 | | | 54 | } |
| e33d237 | | | 55 | |
| b1ace98 | | | 56 | function CanopyFolderIcon({ size }: { size: number }) { |
| b1ace98 | | | 57 | const canopySize = Math.max(8, Math.round(size * 0.58)); |
| b1ace98 | | | 58 | return ( |
| b1ace98 | | | 59 | <span |
| b1ace98 | | | 60 | style={{ |
| b1ace98 | | | 61 | width: size, |
| b1ace98 | | | 62 | height: size, |
| b1ace98 | | | 63 | position: "relative", |
| b1ace98 | | | 64 | display: "inline-flex", |
| b1ace98 | | | 65 | alignItems: "center", |
| b1ace98 | | | 66 | justifyContent: "center", |
| b1ace98 | | | 67 | flexShrink: 0, |
| b1ace98 | | | 68 | }} |
| b1ace98 | | | 69 | > |
| b1ace98 | | | 70 | <FolderIcon size={size} /> |
| b1ace98 | | | 71 | <span |
| b1ace98 | | | 72 | style={{ |
| b1ace98 | | | 73 | position: "absolute", |
| b1ace98 | | | 74 | right: 0, |
| b1ace98 | | | 75 | bottom: 0, |
| b1ace98 | | | 76 | lineHeight: 0, |
| b1ace98 | | | 77 | }} |
| b1ace98 | | | 78 | > |
| b1ace98 | | | 79 | <CanopyLogo size={canopySize} /> |
| b1ace98 | | | 80 | </span> |
| b1ace98 | | | 81 | </span> |
| b1ace98 | | | 82 | ); |
| b1ace98 | | | 83 | } |
| b1ace98 | | | 84 | |
| 2fa7f2c | | | 85 | export function FileIcon({ type, name, className = "", size = 16 }: FileIconProps) { |
| e33d237 | | | 86 | if (type === "tree") { |
| b1ace98 | | | 87 | const isCanopyDir = (name ?? "").toLowerCase() === ".canopy"; |
| e33d237 | | | 88 | return ( |
| e33d237 | | | 89 | <span className={className}> |
| b1ace98 | | | 90 | {isCanopyDir ? <CanopyFolderIcon size={size} /> : <FolderIcon size={size} />} |
| e33d237 | | | 91 | </span> |
| e33d237 | | | 92 | ); |
| e33d237 | | | 93 | } |
| e33d237 | | | 94 | |
| 2fa7f2c | | | 95 | const iconName = getIconName(type, name); |
| 2fa7f2c | | | 96 | |
| 2fa7f2c | | | 97 | return ( |
| b878199 | | | 98 | <span |
| 2fa7f2c | | | 99 | className={className} |
| b878199 | | | 100 | style={{ |
| b878199 | | | 101 | display: "inline-block", |
| b878199 | | | 102 | width: size, |
| b878199 | | | 103 | height: size, |
| b878199 | | | 104 | flexShrink: 0, |
| b878199 | | | 105 | backgroundColor: "var(--text-faint)", |
| b878199 | | | 106 | maskImage: `url(/file-icons/${iconName}.svg)`, |
| b878199 | | | 107 | maskSize: "contain", |
| b878199 | | | 108 | maskRepeat: "no-repeat", |
| b878199 | | | 109 | maskPosition: "center", |
| b878199 | | | 110 | WebkitMaskImage: `url(/file-icons/${iconName}.svg)`, |
| b878199 | | | 111 | WebkitMaskSize: "contain", |
| b878199 | | | 112 | WebkitMaskRepeat: "no-repeat", |
| b878199 | | | 113 | WebkitMaskPosition: "center", |
| b878199 | | | 114 | }} |
| b878199 | | | 115 | role="img" |
| b878199 | | | 116 | aria-hidden="true" |
| 2fa7f2c | | | 117 | /> |
| 2fa7f2c | | | 118 | ); |
| 2fa7f2c | | | 119 | } |