mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-02 01:45:52 +02:00
163 lines
5.1 KiB
TypeScript
163 lines
5.1 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* AttachmentCard — shared file-card row UI (icon + filename + Eye + Download).
|
|
*
|
|
* Subcomponent of the unified `<Attachment>` dispatcher (see attachment.tsx).
|
|
* Rendered for every attachment kind that does not have a richer inline
|
|
* renderer (image / html). Kind-aware routing lives in `<Attachment>` — keep
|
|
* that decision out of this file so this stays a single-purpose row UI.
|
|
*/
|
|
|
|
import { Download, Eye, FileText, Loader2, Trash2 } from "lucide-react";
|
|
import { useT } from "../i18n";
|
|
import { getPreviewKind } from "./utils/preview";
|
|
|
|
interface AttachmentCardChromeProps {
|
|
filename: string;
|
|
uploading?: boolean;
|
|
canPreview: boolean;
|
|
canDownload: boolean;
|
|
canDelete?: boolean;
|
|
onPreview: () => void;
|
|
onDownload: () => void;
|
|
onDelete?: () => void;
|
|
}
|
|
|
|
function AttachmentCardChrome({
|
|
filename,
|
|
uploading,
|
|
canPreview,
|
|
canDownload,
|
|
canDelete,
|
|
onPreview,
|
|
onDownload,
|
|
onDelete,
|
|
}: AttachmentCardChromeProps) {
|
|
const { t } = useT("editor");
|
|
return (
|
|
<div
|
|
className="flex items-center gap-2 rounded-md border border-border bg-muted/50 px-2.5 py-1 transition-colors hover:bg-muted"
|
|
onMouseDown={(e) => e.stopPropagation()}
|
|
>
|
|
{uploading ? (
|
|
<Loader2 className="size-4 shrink-0 animate-spin text-muted-foreground" />
|
|
) : (
|
|
<FileText className="size-4 shrink-0 text-muted-foreground" />
|
|
)}
|
|
<div className="min-w-0 flex-1">
|
|
<p className="truncate text-sm">
|
|
{uploading
|
|
? t(($) => $.file_card.uploading, { filename })
|
|
: filename}
|
|
</p>
|
|
</div>
|
|
{!uploading && canPreview && (
|
|
<button
|
|
type="button"
|
|
className="shrink-0 rounded-md p-1 text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
|
title={t(($) => $.attachment.preview)}
|
|
aria-label={t(($) => $.attachment.preview)}
|
|
onMouseDown={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
onPreview();
|
|
}}
|
|
>
|
|
<Eye className="size-3.5" />
|
|
</button>
|
|
)}
|
|
{!uploading && canDownload && (
|
|
<button
|
|
type="button"
|
|
className="shrink-0 rounded-md p-1 text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
|
title={t(($) => $.image.download)}
|
|
aria-label={t(($) => $.image.download)}
|
|
onMouseDown={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
onDownload();
|
|
}}
|
|
>
|
|
<Download className="size-3.5" />
|
|
</button>
|
|
)}
|
|
{!uploading && canDelete && onDelete && (
|
|
<button
|
|
type="button"
|
|
className="shrink-0 rounded-md p-1 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
|
|
title={t(($) => $.attachment.remove)}
|
|
aria-label={t(($) => $.attachment.remove)}
|
|
onMouseDown={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
onDelete();
|
|
}}
|
|
>
|
|
<Trash2 className="size-3.5" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export interface AttachmentCardProps {
|
|
/** Filename used for icon label and previewable-kind detection. */
|
|
filename: string;
|
|
/** Content type used in addition to filename for previewable-kind detection. */
|
|
contentType?: string;
|
|
/**
|
|
* Attachment id — required when the preview proxy is ID-keyed (text kinds
|
|
* like markdown / html / text). Media kinds (pdf/video/audio) preview from
|
|
* the URL alone.
|
|
*/
|
|
attachmentId?: string;
|
|
/** Download URL — used as a non-null sentinel for the download button. */
|
|
href?: string;
|
|
/** True while a synchronous upload is in flight (file-card NodeView only). */
|
|
uploading?: boolean;
|
|
/** Pressed when the Eye button is clicked. */
|
|
onPreview: () => void;
|
|
/** Pressed when the Download button is clicked. */
|
|
onDownload: () => void;
|
|
/** Optional remove button, used by editable comment/file-card surfaces. */
|
|
onDelete?: () => void;
|
|
}
|
|
|
|
export function AttachmentCard({
|
|
filename,
|
|
contentType = "",
|
|
attachmentId,
|
|
href,
|
|
uploading,
|
|
onPreview,
|
|
onDownload,
|
|
onDelete,
|
|
}: AttachmentCardProps) {
|
|
const kind = filename ? getPreviewKind(contentType, filename) : null;
|
|
// Media kinds (pdf/video/audio) are previewable from a URL alone — the
|
|
// modal renders them as <video>/<audio>/<iframe src=url>. Text kinds
|
|
// (markdown/html/text) need the ID-keyed `/api/attachments/{id}/content`
|
|
// proxy, so they only preview when we have an attachmentId — otherwise
|
|
// the Eye button would call tryOpen, get rejected, and do nothing.
|
|
const isUrlPreviewableKind =
|
|
kind === "pdf" || kind === "video" || kind === "audio";
|
|
const canPreview =
|
|
!!href && kind !== null && (!!attachmentId || isUrlPreviewableKind);
|
|
|
|
return (
|
|
<div className="my-1">
|
|
<AttachmentCardChrome
|
|
filename={filename}
|
|
uploading={uploading}
|
|
canPreview={canPreview}
|
|
canDownload={!!href}
|
|
canDelete={!!onDelete}
|
|
onPreview={onPreview}
|
|
onDownload={onDownload}
|
|
onDelete={onDelete}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|