import { forwardRef, useState } from 'react' import type { HTMLAttributes, KeyboardEvent, MouseEvent, ReactNode } from 'react' import { cn } from '@/lib/utils' import { CloseLine, CodeSSlashLine, FileLine, FileList2Line, TerminalBoxLine, WarningLine, } from '../basic/icons-inline' import { Tooltip } from '../basic/tooltip' import { getFileIconColorClass } from '../lib/file-icon-maps' const ICON_CLASS = 'h-5 w-5' const CODE_EXTENSIONS = new Set(['js', 'jsx', 'ts', 'tsx', 'html', 'css', 'scss', 'sass', 'less', 'vue', 'svelte']) const TERMINAL_EXTENSIONS = new Set(['sh', 'bash', 'zsh', 'fish']) const DOC_EXTENSIONS = new Set(['md', 'mdx', 'txt', 'doc', 'docx', 'pdf', 'ppt', 'pptx']) export type FileCardVariant = 'default' | 'attachment' export type FileCardType = 'file' | 'folder' export interface FileCardProps extends Omit, 'children'> { variant?: FileCardVariant filename: string iconFilename?: string size?: string icon?: ReactNode getFileIcon?: (filename: string, className?: string) => ReactNode onClick?: () => void suffixAction?: ReactNode path?: string type?: FileCardType typeLabel?: string deleted?: boolean deletedTooltip?: string onRemove?: (event: MouseEvent) => void } function getExtension(filename: string): string { const lastDot = filename.lastIndexOf('.') if (lastDot <= 0 || lastDot === filename.length - 1) return '' return filename.slice(lastDot + 1).toLowerCase() } function getDefaultTypeLabel(filename: string, type: FileCardType): string { if (type === 'folder') return 'FOLDER' const ext = getExtension(filename) return ext ? ext.toUpperCase() : 'FILE' } function buildMetadata(typeLabel: string, size?: string): string { return size ? `${typeLabel} ยท ${size}` : typeLabel } function getDefaultIcon(filename: string, className: string): ReactNode { const extension = getExtension(filename) if (CODE_EXTENSIONS.has(extension)) return if (TERMINAL_EXTENSIONS.has(extension)) return if (DOC_EXTENSIONS.has(extension)) return return } function FileCover({ filename, type, deleted, icon, getFileIcon, }: { filename: string type: FileCardType deleted?: boolean icon?: ReactNode getFileIcon?: (filename: string, className?: string) => ReactNode }) { if (type === 'folder') { if (deleted) { return (
) } return (
) } const iconClass = cn(ICON_CLASS, deleted ? 'text-error' : getFileIconColorClass(undefined, filename)) const resolvedIcon = deleted ? ( ) : ( icon ?? (getFileIcon ? getFileIcon(filename, iconClass) : getDefaultIcon(filename, iconClass)) ) return (
{resolvedIcon}
) } export const FileCard = forwardRef( ( { variant = 'default', filename, iconFilename, size, icon, getFileIcon, onClick, suffixAction, path, type = 'file', typeLabel, deleted = false, deletedTooltip = 'File has been deleted', onRemove, className, onMouseEnter, onMouseLeave, ...props }, ref ) => { const iconLookup = iconFilename ?? filename const [isHovered, setIsHovered] = useState(false) const metadata = buildMetadata(typeLabel ?? getDefaultTypeLabel(iconLookup, type), size) const interactive = Boolean(onClick) && !deleted const tooltipContent = deleted ? deletedTooltip : path const handleKeyDown = (event: KeyboardEvent) => { if (!interactive) return if (event.key === 'Enter' || event.key === ' ') { event.preventDefault() onClick?.() } } const handleRemove = (event: MouseEvent) => { event.preventDefault() event.stopPropagation() onRemove?.(event) } const content = ( <>
{filename}
{metadata}
{suffixAction != null && (
) => event.stopPropagation()} > {suffixAction}
)} ) if (variant === 'attachment') { const attachment = (
{ setIsHovered(true) onMouseEnter?.(event) }} onMouseLeave={(event) => { setIsHovered(false) onMouseLeave?.(event) }} >
{content}
{onRemove != null && ( )}
) if (tooltipContent) { return ( {attachment} ) } return attachment } return (
{content}
) } ) FileCard.displayName = 'FileCard'