Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ html.dark {
--bg-subtle: rgba(255,255,255,0.04);
}

.catppuccin-file-icon {
display: block;
flex-shrink: 0;
background: center / contain no-repeat var(--catppuccin-icon-light);
}
html.dark .catppuccin-file-icon {
background-image: var(--catppuccin-icon-dark);
}

/* Theme toggle: circular wipe via View Transitions API.
Disable the UA cross-fade; the actual clip-path animation is driven by
Element.animate() in hooks/useTheme.ts after transition.ready resolves. */
Expand Down
331 changes: 104 additions & 227 deletions components/FileIcons.tsx
Original file line number Diff line number Diff line change
@@ -1,249 +1,126 @@
// Flat monochrome file & folder icons — all use currentColor / var(--text-dim)
import type { CSSProperties } from "react";


interface IconProps {
size?: number;
}

const DIM = "var(--text-dim)";
type CatppuccinIconName =
| "_file"
| "_folder"
| "_folder_open"
| "bash"
| "config"
| "css"
| "database"
| "docker"
| "env"
| "git"
| "graphql"
| "html"
| "javascript"
| "javascript-react"
| "json"
| "lock"
| "npm-lock"
| "bun-lock"
| "next"
| "eslint"
| "markdown"
| "ms-word"
| "pdf"
| "python"
| "rust"
| "sass"
| "terraform"
| "toml"
| "typescript"
| "typescript-react"
| "yaml"
| "go";

// ── Folder ────────────────────────────────────────────────────────────────
const CATPPUCCIN_ICONS_ROOT = "/icons/catppuccin";

export function FolderIcon({ size = 14, open = false }: IconProps & { open?: boolean }) {
if (open) {
return (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none">
<path d="M1 4.5A1 1 0 0 1 2 3.5H5.5L7 5h7.5v1H1V4.5Z" fill={DIM} />
<path d="M1 6h14.5L14 13H2L1 6Z" stroke={DIM} strokeWidth="1" fill={DIM} fillOpacity="0.12" />
</svg>
);
}
return (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none">
<path d="M1 4.5A1 1 0 0 1 2 3.5H5.5L7 5H14a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4.5Z"
stroke={DIM} strokeWidth="1" fill={DIM} fillOpacity="0.1" />
</svg>
);
}

// ── Generic file (fallback) ────────────────────────────────────────────────
function CatppuccinIcon({ name, size = 14 }: IconProps & { name: CatppuccinIconName }) {
const style = {
width: size,
height: size,
"--catppuccin-icon-light": `url(${CATPPUCCIN_ICONS_ROOT}/latte/${name}.svg)`,
"--catppuccin-icon-dark": `url(${CATPPUCCIN_ICONS_ROOT}/mocha/${name}.svg)`,
} as CSSProperties;

export function GenericFileIcon({ size = 14 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none">
<path d="M3 2h7l3 3v9H3V2Z" stroke={DIM} strokeWidth="1" fill={DIM} fillOpacity="0.08" />
<path d="M10 2v3h3" stroke={DIM} strokeWidth="1" fill="none" strokeLinejoin="round" />
</svg>
<span
aria-hidden="true"
className="catppuccin-file-icon"
style={style}
/>
);
}

// ── File with label text (used for most types) ────────────────────────────
// Renders the file outline + a short text badge
export function FolderIcon({ size = 14, open = false }: IconProps & { open?: boolean }) {
return <CatppuccinIcon name={open ? "_folder_open" : "_folder"} size={size} />;
}

function LabelFileIcon({ label, size = 14 }: { label: string; size?: number }) {
const s = size / 14; // scale factor
return (
<svg width={size} height={size} viewBox="0 0 14 14" fill="none">
<path d="M2.5 1h6l3 3v9h-9V1Z" stroke={DIM} strokeWidth="0.9" fill={DIM} fillOpacity="0.07" strokeLinejoin="round" />
<path d="M8.5 1v3h3" stroke={DIM} strokeWidth="0.9" fill="none" strokeLinejoin="round" />
<text
x="7" y="9.5"
textAnchor="middle"
fontSize={3.4 * s}
fontFamily="var(--font-mono), monospace"
fontWeight="600"
fill={DIM}
letterSpacing="0"
>{label}</text>
</svg>
);
export function GenericFileIcon({ size = 14 }: IconProps) {
return <CatppuccinIcon name="_file" size={size} />;
}

// ── Specific icons ────────────────────────────────────────────────────────
const EXTENSION_ICONS: Record<string, CatppuccinIconName> = {
ts: "typescript",
tsx: "typescript-react",
js: "javascript",
mjs: "javascript",
cjs: "javascript",
jsx: "javascript-react",
py: "python",
json: "json",
jsonl: "json",
css: "css",
less: "css",
scss: "sass",
html: "html",
htm: "html",
md: "markdown",
mdx: "markdown",
yaml: "yaml",
yml: "yaml",
toml: "toml",
sh: "bash",
bash: "bash",
zsh: "bash",
fish: "bash",
rs: "rust",
go: "go",
sql: "database",
graphql: "graphql",
gql: "graphql",
tf: "terraform",
hcl: "terraform",
docx: "ms-word",
pdf: "pdf",
lock: "lock",
};

export function TypeScriptIcon({ size = 14 }: IconProps) {
return <LabelFileIcon label="TS" size={size} />;
}
export function TypeScriptReactIcon({ size = 14 }: IconProps) {
return <LabelFileIcon label="TSX" size={size} />;
}
export function JavaScriptIcon({ size = 14 }: IconProps) {
return <LabelFileIcon label="JS" size={size} />;
}
export function JavaScriptReactIcon({ size = 14 }: IconProps) {
return <LabelFileIcon label="JSX" size={size} />;
}
export function PythonIcon({ size = 14 }: IconProps) {
return <LabelFileIcon label="PY" size={size} />;
}
export function JsonIcon({ size = 14 }: IconProps) {
return <LabelFileIcon label="{}" size={size} />;
}
export function CssIcon({ size = 14 }: IconProps) {
return <LabelFileIcon label="CSS" size={size} />;
}
export function ScssIcon({ size = 14 }: IconProps) {
return <LabelFileIcon label="SC" size={size} />;
}
export function HtmlIcon({ size = 14 }: IconProps) {
return <LabelFileIcon label="HTM" size={size} />;
}
export function MarkdownIcon({ size = 14 }: IconProps) {
// file outline + M↓ symbol
return (
<svg width={size} height={size} viewBox="0 0 14 14" fill="none">
<path d="M2.5 1h6l3 3v9h-9V1Z" stroke={DIM} strokeWidth="0.9" fill={DIM} fillOpacity="0.07" strokeLinejoin="round" />
<path d="M8.5 1v3h3" stroke={DIM} strokeWidth="0.9" fill="none" strokeLinejoin="round" />
{/* M */}
<path d="M3.5 9.5V7l1.5 1.5L6.5 7v2.5" stroke={DIM} strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" fill="none" />
{/* down arrow */}
<path d="M8 7v2.5M7 9l1 1.5 1-1.5" stroke={DIM} strokeWidth="0.9" strokeLinecap="round" strokeLinejoin="round" fill="none" />
</svg>
);
}
export function YamlIcon({ size = 14 }: IconProps) {
return <LabelFileIcon label="YML" size={size} />;
}
export function TomlIcon({ size = 14 }: IconProps) {
return <LabelFileIcon label="TOM" size={size} />;
}
export function ShellIcon({ size = 14 }: IconProps) {
// file outline + > prompt
return (
<svg width={size} height={size} viewBox="0 0 14 14" fill="none">
<path d="M2.5 1h6l3 3v9h-9V1Z" stroke={DIM} strokeWidth="0.9" fill={DIM} fillOpacity="0.07" strokeLinejoin="round" />
<path d="M8.5 1v3h3" stroke={DIM} strokeWidth="0.9" fill="none" strokeLinejoin="round" />
<path d="M4 7.5l2 1.5-2 1.5" stroke={DIM} strokeWidth="0.95" strokeLinecap="round" strokeLinejoin="round" fill="none" />
<path d="M7.5 10.5h2.5" stroke={DIM} strokeWidth="0.95" strokeLinecap="round" />
</svg>
);
}
export function RustIcon({ size = 14 }: IconProps) {
return <LabelFileIcon label="RS" size={size} />;
}
export function GoIcon({ size = 14 }: IconProps) {
return <LabelFileIcon label="GO" size={size} />;
}
export function SqlIcon({ size = 14 }: IconProps) {
return <LabelFileIcon label="SQL" size={size} />;
}
export function GraphqlIcon({ size = 14 }: IconProps) {
return <LabelFileIcon label="GQL" size={size} />;
}
export function TerraformIcon({ size = 14 }: IconProps) {
return <LabelFileIcon label="TF" size={size} />;
}
export function DockerfileIcon({ size = 14 }: IconProps) {
// file outline + container stack
return (
<svg width={size} height={size} viewBox="0 0 14 14" fill="none">
<path d="M2.5 1h6l3 3v9h-9V1Z" stroke={DIM} strokeWidth="0.9" fill={DIM} fillOpacity="0.07" strokeLinejoin="round" />
<path d="M8.5 1v3h3" stroke={DIM} strokeWidth="0.9" fill="none" strokeLinejoin="round" />
<rect x="3.5" y="6.5" width="2" height="1.5" rx="0.3" stroke={DIM} strokeWidth="0.8" />
<rect x="6" y="6.5" width="2" height="1.5" rx="0.3" stroke={DIM} strokeWidth="0.8" />
<rect x="3.5" y="8.5" width="2" height="1.5" rx="0.3" stroke={DIM} strokeWidth="0.8" />
</svg>
);
function getSpecialFileIcon(name: string): CatppuccinIconName | undefined {
if (name === "dockerfile" || name.startsWith("dockerfile.")) return "docker";
if (name === ".env" || name.startsWith(".env.")) return "env";
if ([".gitignore", ".gitattributes", ".gitmodules"].includes(name)) return "git";
if (name === "package-lock.json") return "npm-lock";
if (name === "bun.lock") return "bun-lock";
if (["next.config.js", "next.config.mjs", "next.config.cjs", "next.config.ts"].includes(name)) return "next";
if ([".eslintrc", ".eslintrc.js", ".eslintrc.json", ".eslintrc.yml", "eslint.config.mjs", "eslint.config.js"].includes(name)) return "eslint";
if (["yarn.lock", "pnpm-lock.yaml", "cargo.lock"].includes(name)) return "lock";
if (name.endsWith(".config.ts") || name.endsWith(".config.js") || name.endsWith(".config.mjs") || name.endsWith(".config.cjs")) return "config";
return undefined;
}
export function EnvIcon({ size = 14 }: IconProps) {
// file outline + key symbol
return (
<svg width={size} height={size} viewBox="0 0 14 14" fill="none">
<path d="M2.5 1h6l3 3v9h-9V1Z" stroke={DIM} strokeWidth="0.9" fill={DIM} fillOpacity="0.07" strokeLinejoin="round" />
<path d="M8.5 1v3h3" stroke={DIM} strokeWidth="0.9" fill="none" strokeLinejoin="round" />
<circle cx="5.5" cy="8.5" r="1.5" stroke={DIM} strokeWidth="0.9" />
<path d="M7 8.5h2.5M8.5 8.5v1.5" stroke={DIM} strokeWidth="0.9" strokeLinecap="round" />
</svg>
);
}
export function GitIcon({ size = 14 }: IconProps) {
// file outline + git branch lines
return (
<svg width={size} height={size} viewBox="0 0 14 14" fill="none">
<path d="M2.5 1h6l3 3v9h-9V1Z" stroke={DIM} strokeWidth="0.9" fill={DIM} fillOpacity="0.07" strokeLinejoin="round" />
<path d="M8.5 1v3h3" stroke={DIM} strokeWidth="0.9" fill="none" strokeLinejoin="round" />
<circle cx="5" cy="6.5" r="1" stroke={DIM} strokeWidth="0.85" />
<circle cx="9" cy="6.5" r="1" stroke={DIM} strokeWidth="0.85" />
<circle cx="5" cy="10" r="1" stroke={DIM} strokeWidth="0.85" />
<path d="M5 7.5V9" stroke={DIM} strokeWidth="0.85" strokeLinecap="round" />
<path d="M9 7.5v.5a2 2 0 0 1-2 2H6" stroke={DIM} strokeWidth="0.85" strokeLinecap="round" fill="none" />
</svg>
);
}
export function LockFileIcon({ size = 14 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 14 14" fill="none">
<path d="M2.5 1h6l3 3v9h-9V1Z" stroke={DIM} strokeWidth="0.9" fill={DIM} fillOpacity="0.07" strokeLinejoin="round" />
<path d="M8.5 1v3h3" stroke={DIM} strokeWidth="0.9" fill="none" strokeLinejoin="round" />
<rect x="4.5" y="8.5" width="5" height="3" rx="0.6" stroke={DIM} strokeWidth="0.9" />
<path d="M5.5 8.5V7.5a1.5 1.5 0 0 1 3 0v1" stroke={DIM} strokeWidth="0.9" strokeLinecap="round" fill="none" />
</svg>
);
}
export function DocFileIcon({ size = 14 }: IconProps) {
return <LabelFileIcon label="DOC" size={size} />;
}
export function PdfFileIcon({ size = 14 }: IconProps) {
return <LabelFileIcon label="PDF" size={size} />;
}
export function ConfigIcon({ size = 14 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 14 14" fill="none">
<path d="M2.5 1h6l3 3v9h-9V1Z" stroke={DIM} strokeWidth="0.9" fill={DIM} fillOpacity="0.07" strokeLinejoin="round" />
<path d="M8.5 1v3h3" stroke={DIM} strokeWidth="0.9" fill="none" strokeLinejoin="round" />
<circle cx="7" cy="8.5" r="1.3" stroke={DIM} strokeWidth="0.9" />
<path d="M7 6.5v.7M7 10.3v.7M5 8.5h.7M8.3 8.5H9M5.5 6.9l.5.5M8.5 9.6l-.5-.5M5.5 10.1l.5-.5M8.5 7.4l-.5.5"
stroke={DIM} strokeWidth="0.8" strokeLinecap="round" />
</svg>
);
}

// ── Main resolver ─────────────────────────────────────────────────────────

export function getFileIcon(name: string, size = 14): React.ReactNode {
const lower = name.toLowerCase();
const ext = lower.split(".").pop() ?? "";

if (lower === "dockerfile" || lower.startsWith("dockerfile.")) return <DockerfileIcon size={size} />;
if (lower === ".env" || lower.startsWith(".env.")) return <EnvIcon size={size} />;
if (lower === ".gitignore" || lower === ".gitattributes" || lower === ".gitmodules") return <GitIcon size={size} />;
if (lower === "package-lock.json" || lower === "yarn.lock" || lower === "bun.lock" || lower === "pnpm-lock.yaml" || lower === "cargo.lock") return <LockFileIcon size={size} />;
if (lower.endsWith(".config.ts") || lower.endsWith(".config.js") || lower.endsWith(".config.mjs") || lower.endsWith(".config.cjs")) return <ConfigIcon size={size} />;
if ([".eslintrc", ".eslintrc.js", ".eslintrc.json", ".eslintrc.yml", "eslint.config.mjs", "eslint.config.js"].includes(lower)) return <ConfigIcon size={size} />;
const specialIcon = getSpecialFileIcon(lower);
if (specialIcon) return <CatppuccinIcon name={specialIcon} size={size} />;

switch (ext) {
case "ts": return <TypeScriptIcon size={size} />;
case "tsx": return <TypeScriptReactIcon size={size} />;
case "js":
case "mjs":
case "cjs": return <JavaScriptIcon size={size} />;
case "jsx": return <JavaScriptReactIcon size={size} />;
case "py": return <PythonIcon size={size} />;
case "json":
case "jsonl": return <JsonIcon size={size} />;
case "css":
case "less": return <CssIcon size={size} />;
case "scss": return <ScssIcon size={size} />;
case "html":
case "htm": return <HtmlIcon size={size} />;
case "md":
case "mdx": return <MarkdownIcon size={size} />;
case "yaml":
case "yml": return <YamlIcon size={size} />;
case "toml": return <TomlIcon size={size} />;
case "sh":
case "bash":
case "zsh":
case "fish": return <ShellIcon size={size} />;
case "rs": return <RustIcon size={size} />;
case "go": return <GoIcon size={size} />;
case "sql": return <SqlIcon size={size} />;
case "graphql":
case "gql": return <GraphqlIcon size={size} />;
case "tf":
case "hcl": return <TerraformIcon size={size} />;
case "docx": return <DocFileIcon size={size} />;
case "pdf": return <PdfFileIcon size={size} />;
case "lock": return <LockFileIcon size={size} />;
default: return <GenericFileIcon size={size} />;
}
const ext = lower.split(".").pop() ?? "";
const icon = EXTENSION_ICONS[ext];
return icon ? <CatppuccinIcon name={icon} size={size} /> : <GenericFileIcon size={size} />;
}
22 changes: 22 additions & 0 deletions public/icons/catppuccin/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
MIT License

Copyright (c) 2023 Catppuccin
Copyright (c) 2023 thang-nm

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading