"use client"; /** * One document in the vault timeline (compact-density Card) plus the two * transient upload-state cards (in-flight with a live progress ring, failed * with the translated §3.2 reason). All three share the card footprint so an * optimistic entry morphs into the stored row without the grid jumping. * * Anatomy per the design standards: kind icon `text-foreground size-5`, * title in foreground (`text-sm font-medium`, truncated), one muted meta * line (date · size · filename), condition-tag pills, an attachment-class * badge for download-only formats, and a selection checkbox that appears on * hover / focus / while selected. The whole card is clickable through an * invisible overlay button; the checkbox floats above it. */ import { Download, ScanSearch, Sparkles, X } from "lucide-react"; import { useRef } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { useFormatters, useTranslations } from "@/lib/i18n/context"; import { cn } from "@/lib/utils"; import { isAiReadSource, type InboundDocumentDto, } from "@/lib/validations/inbound-documents"; import { DOCUMENT_KIND_ICONS } from "./document-kind-meta"; import type { UploadQueueItem } from "./use-document-upload"; import { documentDateKey, formatBytes } from "./vault-utils"; /** Press-and-hold duration that flips a touch press into "select" mode. */ const LONG_PRESS_MS = 500; export function DocumentCard({ document, selected, onToggleSelected, onOpen, onDelete, highlighted, tabIndex = 0, onCardFocus, onPrefetch, }: { document: InboundDocumentDto; selected: boolean; /** `range` = extend the selection from the last anchor (shift-click). */ onToggleSelected: (id: string, range?: boolean) => void; onOpen: (id: string) => void; /** Delete key on the focused card — undo-able delete owned by the page. */ onDelete?: (id: string) => void; /** Brief ring after a duplicate upload resolved to this existing row. */ highlighted: boolean; /** Roving-tabindex slot from the timeline (0 = the one tabbable card). */ tabIndex?: number; /** The open-button gained focus (keyboard/roving bookkeeping). */ onCardFocus?: (id: string) => void; /** Hover/focus intent — prefetches the detail metadata (never the blob). */ onPrefetch?: (id: string) => void; }) { const { t, locale } = useTranslations(); const format = useFormatters(); // Touch long-press selects instead of opening; the click that the // browser fires after the release is swallowed once. const longPressTimer = useRef | null>(null); const longPressFired = useRef(false); const cancelLongPress = () => { if (longPressTimer.current) { clearTimeout(longPressTimer.current); longPressTimer.current = null; } }; const title = document.title ?? document.filename ?? t("documents.card.untitled"); const Icon = DOCUMENT_KIND_ICONS[document.kind]; const date = format.date(`${documentDateKey(document)}T12:00:00.000Z`); const size = formatBytes(document.byteSize, locale); const showFilename = document.filename !== null && document.filename !== title; return (

{title}

{ e.preventDefault(); onToggleSelected(document.id, e.shiftKey); }} aria-label={t("documents.card.selectLabel", { title })} className={cn( "relative z-10 shrink-0 transition-opacity", "opacity-0 group-focus-within:opacity-100 group-hover:opacity-100", selected && "opacity-100", )} />

{date} · {size} {showFilename ? ` · ${document.filename}` : ""}

{document.conditionLinks.length > 0 || document.servingClass === "attachment" || document.hasContentIndex ? (
{document.conditionLinks.map((link) => ( {link.name} ))} {document.servingClass === "attachment" ? ( {t("documents.card.attachmentBadge")} ) : null} {document.hasContentIndex ? ( // Subtle searchable marker — icon-only to keep the row calm; the // label rides an accessible name. An AI-read document gets the // highlighted Sparkles (primary) so the richer read is legible at // a glance; a locally-indexed one keeps the quiet muted glass. isAiReadSource(document.contentIndexSource) ? ( {t("documents.card.aiReadBadge")} ) : ( {t("documents.card.searchableBadge")} ) ) : null}
) : null}
{/* Whole-card click target. Painted last so it sits above the content (the checkbox stays reachable via its own z-10). Keyboard contract (roving tabindex owned by the timeline): Enter opens (native click), Space toggles selection, Delete removes with undo. Touch: press-and-hold selects instead of opening. */} ) : null} {failureCopy ? (

{failureCopy}

) : null}
); }