// File-list glyphs + per-category colors. Category colors // are fixed tokens (never follow the accent) so a folder is always folder-blue. // Only the icon glyph is colored; row text stays neutral. Docs and unknown // files stay muted on purpose — sparing color is what keeps it scannable. import type { FileEntry } from '@shared/types'; type Category = 'image' | 'audio' | 'video' | 'archive' | 'code' | 'doc'; // Class strings are literals so the Tailwind scanner picks them up. const MUTED = 'shrink-0 text-muted-foreground'; const FOLDER = 'shrink-0 text-icon-folder'; const LINK = 'shrink-0 text-icon-link'; const CATEGORIES: Record = { image: { glyph: 'tdesignImage', class: 'shrink-0 text-icon-image', extensions: 'png jpg jpeg gif webp avif svg ico icns bmp tiff tif heic heif raw'.split(' '), }, audio: { glyph: 'tdesignMusic', class: 'shrink-0 text-icon-media', extensions: 'mp3 wav flac aac ogg oga m4a opus wma aiff aif mid midi'.split(' '), }, video: { glyph: 'tdesignVideo', class: 'shrink-0 text-icon-media', extensions: 'mp4 mkv mov avi webm m4v mpg mpeg wmv flv 3gp'.split(' '), }, archive: { glyph: 'tdesignFileZip', class: 'shrink-0 text-icon-archive', extensions: 'zip tar gz tgz bz2 xz zst 7z rar dmg iso jar deb rpm pkg apk'.split(' '), }, code: { glyph: 'tdesignCode', class: 'shrink-0 text-icon-code', // ".ts" is TypeScript here, not an MPEG transport stream (deliberate) extensions: ( 'js mjs cjs ts tsx jsx json jsonc yaml yml toml xml html htm css scss sass less ' + 'py rb go rs java kt kts swift c h cpp hpp cc m mm cs php sh bash zsh fish ps1 bat ' + 'sql lua pl r vue svelte astro ini conf cfg env plist gradle cmake nix proto graphql ' + 'dart ex exs zig lock makefile dockerfile zshrc bashrc bash_profile profile ' + 'gitignore gitconfig gitattributes editorconfig npmrc nvmrc' ).split(' '), }, doc: { glyph: 'tdesignFile1', class: MUTED, // docs keep the neutral color; only the glyph changes extensions: 'md markdown txt rtf pdf doc docx xls xlsx ppt pptx csv tsv log odt ods odp epub'.split(' '), }, }; const EXT_CATEGORY = new Map(); for (const category of Object.keys(CATEGORIES) as Category[]) { for (const ext of CATEGORIES[category].extensions) EXT_CATEGORY.set(ext, category); } /** "a.tar.gz" -> "gz"; ".zshrc" -> "zshrc"; "Makefile" (no dot) -> "makefile". */ function categoryOf(name: string): Category | null { const dot = name.lastIndexOf('.'); const key = (dot >= 0 ? name.slice(dot + 1) : name).toLowerCase(); return EXT_CATEGORY.get(key) ?? null; } const KIND_KEYS: Record = { image: 'image', audio: 'audio', video: 'video', archive: 'archive', code: 'code', doc: 'document', }; /** * i18n key suffix for the optional Kind column (category-based, like the * icons); '' for the synthetic '..' row. The caller resolves `kinds.`. */ export function fileKindKey(entry: FileEntry): string { if (entry.name === '..') return ''; if (entry.type === 'dir') return 'folder'; if (entry.type === 'symlink') return 'symlink'; const category = categoryOf(entry.name); return category ? KIND_KEYS[category] : 'file'; } export function fileGlyph(entry: FileEntry): string { if (entry.name === '..') return 'tdesignArrowUp'; if (entry.type === 'dir') return 'tdesignFolder'; if (entry.type === 'symlink') return 'tdesignLink'; const category = categoryOf(entry.name); return category ? CATEGORIES[category].glyph : 'tdesignFile'; } export function fileIconClass(entry: FileEntry): string { if (entry.name === '..') return MUTED; if (entry.type === 'dir') return FOLDER; if (entry.type === 'symlink') return LINK; const category = categoryOf(entry.name); return category ? CATEGORIES[category].class : MUTED; }