File size: 4,530 Bytes
0b9dc2e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import { Download, File, FileAudio, FileImage, FileText, FileVideo } from 'lucide-react';
import * as mime from 'mime-types';

interface FileIconProps {
	/** The MIME type, e.g. `application/pdf`. */
	mediaType: string;
	/** Optional CSS classes forwarded to the icon. */
	className?: string;
}

/**

 * Render a representative icon for a file based on its MIME type.

 *

 * @param root0 - The component props.

 * @param root0.mediaType - The MIME type of the file.

 * @param root0.className - CSS classes forwarded to the icon.

 * @returns A lucide icon element appropriate for the file kind.

 */
function FileIcon({ mediaType, className }: FileIconProps) {
	switch (mediaType.split('/')[0]) {
		case 'image':
			return <FileImage className={className} />;
		case 'audio':
			return <FileAudio className={className} />;
		case 'video':
			return <FileVideo className={className} />;
		case 'text':
			return <FileText className={className} />;
		default:
			return mediaType === 'application/pdf' ? (
				<FileText className={className} />
			) : (
				<File className={className} />
			);
	}
}

/**

 * Classify a file href for safe, correct linking.

 *

 * - `safe`: only `http`/`https`/`data`/`blob` schemes become a clickable

 *   link, so a malicious model-supplied URL (e.g. `javascript:`) can't turn

 *   the card into a click-to-execute vector.

 * - `downloadable`: the HTML `download` attribute is only honoured for

 *   same-origin, `blob:`, and `data:` URLs; cross-origin remote URLs ignore

 *   it and just open. We only set `download` when it will actually work, and

 *   open remote URLs in a new tab instead.

 *

 * @param href - The candidate href (remote URL or `data:` URL).

 * @returns Whether the href is safe to link and whether `download` applies.

 */
function classifyHref(href: string): { safe: boolean; downloadable: boolean } {
	let url: URL;
	try {
		url = new URL(href, window.location.origin);
	} catch {
		return { safe: false, downloadable: false };
	}
	const safe = ['http:', 'https:', 'data:', 'blob:'].includes(url.protocol);
	const downloadable =
		url.protocol === 'data:' ||
		url.protocol === 'blob:' ||
		url.origin === window.location.origin;
	return { safe, downloadable };
}

interface FileAttachmentProps {
	/** Display name of the file (falls back to a name generated from the extension). */
	name?: string;
	/** Resolved href — a remote URL or a `data:` URL — used for download. */
	href: string;
	/** MIME type of the file, e.g. `application/pdf`. */
	mediaType: string;
}

/**

 * A compact, downloadable card for a non-previewable file attachment

 * (PDF, Word, Excel, plain-text blobs, …).

 *

 * Image / audio / video data blocks are rendered inline by the caller;

 * every other MIME type falls back to this card instead of rendering

 * nothing at all.

 *

 * @param root0 - The component props.

 * @param root0.name - Display name of the file.

 * @param root0.href - Download href (remote URL or `data:` URL).

 * @param root0.mediaType - MIME type of the file.

 * @returns A downloadable file-attachment card.

 */
export function FileAttachment({ name, href, mediaType }: FileAttachmentProps) {
	const ext = (mime.extension(mediaType) || '').toUpperCase();
	const displayName = name || (ext ? `file.${ext.toLowerCase()}` : 'file');
	const { safe, downloadable } = classifyHref(href);

	// Only safe schemes become a real link. Downloadable hrefs (data:/blob:/
	// same-origin) get `download`; remote URLs open in a new tab, where the
	// `download` attribute would be ignored anyway.
	const linkProps = !safe
		? {}
		: downloadable
			? { href, download: displayName }
			: { href, target: '_blank' as const, rel: 'noopener noreferrer' };

	return (
		<a

			{...linkProps}

			className="group flex w-fit max-w-xs items-center gap-3 rounded-lg border bg-muted/40 px-3 py-2 no-underline transition-colors hover:bg-muted"

		>

			<span className="flex size-9 shrink-0 items-center justify-center rounded-md bg-background text-muted-foreground">

				<FileIcon mediaType={mediaType} className="size-4" />

			</span>

			<span className="flex min-w-0 flex-col">

				<span className="truncate text-sm font-medium text-foreground">{displayName}</span>

				{ext && <span className="text-xs text-muted-foreground">{ext}</span>}

			</span>

			<Download className="size-4 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100" />

		</a>
	);
}