import { type JSX, createContext, useContext, splitProps, Show } from 'solid-js'; import { cn } from '../utils/cn'; import { Button } from '../ui/button'; import { HoverCardRoot, HoverCardTrigger, HoverCardContent } from '../ui/hover-card'; import { FileText, Globe, Image as ImageIcon, Music2, Paperclip, Video, X, } from 'lucide-solid'; // ============================================================================ // Types // ============================================================================ export interface AttachmentData { id: string; type: 'file' | 'source-document'; filename?: string; mediaType?: string; url?: string; title?: string; } export type AttachmentMediaCategory = 'image' | 'video' | 'audio' | 'document' | 'source' | 'unknown'; export type AttachmentVariant = 'grid' | 'inline' | 'list'; const mediaCategoryIcons: Record = { audio: Music2, document: FileText, image: ImageIcon, source: Globe, unknown: Paperclip, video: Video, }; // ============================================================================ // Utility Functions // ============================================================================ export const getMediaCategory = (data: AttachmentData): AttachmentMediaCategory => { if (data.type === 'source-document') { return 'source'; } const mediaType = data.mediaType ?? ''; if (mediaType.startsWith('image/')) return 'image'; if (mediaType.startsWith('video/')) return 'video'; if (mediaType.startsWith('audio/')) return 'audio'; if (mediaType.startsWith('application/') || mediaType.startsWith('text/')) return 'document'; return 'unknown'; }; export const getAttachmentLabel = (data: AttachmentData): string => { if (data.type === 'source-document') { return data.title || data.filename || 'Source'; } const category = getMediaCategory(data); return data.filename || (category === 'image' ? 'Image' : 'Attachment'); }; // ============================================================================ // Contexts // ============================================================================ interface AttachmentsContextValue { variant: AttachmentVariant; } const AttachmentsContext = createContext(); interface AttachmentContextValue { data: AttachmentData; mediaCategory: AttachmentMediaCategory; onRemove?: () => void; variant: AttachmentVariant; } const AttachmentContext = createContext(); // ============================================================================ // Hooks // ============================================================================ export const useAttachmentsContext = () => useContext(AttachmentsContext) ?? { variant: 'grid' as const }; export const useAttachmentContext = () => { const ctx = useContext(AttachmentContext); if (!ctx) { throw new Error('Attachment components must be used within '); } return ctx; }; // ============================================================================ // Attachments - Container // ============================================================================ export interface AttachmentsProps extends JSX.HTMLAttributes { variant?: AttachmentVariant; } function Attachments(props: AttachmentsProps) { const [local, rest] = splitProps(props, ['variant', 'class', 'children']); const variant = () => local.variant ?? 'grid'; return (
{local.children}
); } // ============================================================================ // Attachment - Item // ============================================================================ export interface AttachmentProps extends JSX.HTMLAttributes { data: AttachmentData; onRemove?: () => void; } function Attachment(props: AttachmentProps) { const [local, rest] = splitProps(props, ['data', 'onRemove', 'class', 'children']); // Read the getter reactively — DON'T destructure, or the variant is captured // once and the item never re-lays-out when the container variant changes. const ctx = useAttachmentsContext(); const mediaCategory = () => getMediaCategory(local.data); return (
{local.children}
); } // ============================================================================ // AttachmentPreview - Media preview // ============================================================================ export interface AttachmentPreviewProps extends JSX.HTMLAttributes { fallbackIcon?: JSX.Element; } function AttachmentPreview(props: AttachmentPreviewProps) { const [local, rest] = splitProps(props, ['fallbackIcon', 'class']); const ctx = useAttachmentContext(); const iconSize = () => ctx.variant === 'inline' ? 'size-3' : 'size-4'; const renderIcon = (Icon: typeof ImageIcon) => ( ); const renderContent = () => { if (ctx.mediaCategory === 'image' && ctx.data.type === 'file' && ctx.data.url) { return ctx.variant === 'grid' ? ( {ctx.data.filename ) : ( {ctx.data.filename ); } if (ctx.mediaCategory === 'video' && ctx.data.type === 'file' && ctx.data.url) { return