import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { DownloadIcon, EditIcon, ArrowLeftIcon, ArrowRightIcon, ChevronsRightIcon, AnalyzeIcon, WindowMaximizeIcon, TextScanIcon, } from '../ui/icons'; import { getTagColorStyle } from '../utils/colors'; import { formatFileSize } from '../utils/format'; import { resolveDocumentAssetUrl, createAssetView } from '../asset_manager'; import { useAssetNavigator } from '../hooks/useAssetNavigator'; import { describeDocumentSummary } from '../documents/documentSummary'; import { createDocumentActionState } from '../documents/documentActions'; import PreviewZoomOverlay from './PreviewZoomOverlay'; const MAX_PREVIEW_STACK_ITEMS = 15; const derivePreviewOrientation = (metadata) => { const width = Number(metadata?.width); const height = Number(metadata?.height); if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) { return width >= height ? 'landscape' : 'portrait'; } return 'landscape'; }; const sortCorrespondents = (entries = []) => entries .filter((entry) => entry && entry.name) .map(({ id, name, count }) => ({ id, name, count })) .sort((a, b) => a.name.localeCompare(b.name)); const resolveDocumentType = (doc) => { if (!doc) { return null; } const type = doc.document_type; const fallbackId = doc.document_type_id ?? null; if (!type) { return null; } if (typeof type === 'object' && typeof type.name === 'string') { const name = type.name.trim(); if (!name) { return null; } return { id: type.id ?? fallbackId, name }; } return null; }; const CorrespondentPills = ({ entries = [], onRemove, showCount = false }) => (
{entries.length ? ( entries.map((entry, index) => { const key = entry.id ?? `${entry.name}-${index}`; return ( {entry.name} {showCount && entry.count ? ` (${entry.count})` : ''} {onRemove ? ( ) : null} ); }) ) : ( No correspondents yet. )}
); const TagSection = ({ title, tags = [], onRemove, onAdd, emptyMessage = 'No tags yet.', addPlaceholder = 'Add or create tag', addButtonLabel = 'Add', datalistId, datalistOptions = [], className, }) => (
{title}
{tags.length ? ( tags.map((tag) => { const key = tag.id ?? tag.label; const style = getTagColorStyle(tag.color); const removable = Boolean(onRemove); const className = removable ? 'badge tag-chip tag-chip--removable' : 'badge tag-chip'; return ( {tag.label} {removable ? ( ) : null} ); }) ) : ( {emptyMessage} )}
{onAdd ? (
{ event.preventDefault(); const input = event.currentTarget.elements.tag; const value = input.value.trim(); if (!value) return; onAdd({ value, input }); }} > {datalistId ? ( {datalistOptions.map((option) => ( ) : null}
) : null}
); const CorrespondentSection = ({ title, entries = [], onRemove, onAdd, showCount = false, addPlaceholder = 'Add or create correspondent', addButtonLabel = 'Add', datalistId, datalistOptions = [], className, }) => (
{title}
{onAdd ? (
{ event.preventDefault(); const form = event.currentTarget; const nameInput = form.elements.correspondent; const value = nameInput.value.trim(); if (!value) return; onAdd({ name: value, input: nameInput }); form.reset(); }} > {datalistId ? ( {datalistOptions.map((name) => ( ) : null}
) : null}
); const computeStackAngle = (docId, index) => { if (index === 0) return 0; let hash = 0; const source = docId || `stack-${index}`; for (let i = 0; i < source.length; i += 1) { hash = (hash * 31 + source.charCodeAt(i)) % 997; } const magnitude = Math.max(3, (hash % 13) + 3); const sign = index % 2 === 0 ? 1 : -1; return magnitude * sign; }; const PreviewStack = ({ items = [], maxItems = MAX_PREVIEW_STACK_ITEMS, emptyMessage = 'Preview unavailable', onItemActivate, onOpenPreview, onZoomPreview, }) => { const limited = useMemo(() => items.slice(0, maxItems), [items, maxItems]); const hasMultiple = limited.length > 1; const preparedItems = useMemo( () => limited.map((entry, index) => ({ entry, angle: index === 0 ? 0 : computeStackAngle(entry.id, index), })), [limited], ); if (!limited.length) { return {emptyMessage}; } return (
{preparedItems.map(({ entry, angle }, index) => { const transform = hasMultiple ? `translate(-50%, -50%) rotate(${angle}deg)` : 'translate(-50%, -50%)'; const isFront = index === 0; return (
{entry.alt} { event.stopPropagation(); if (isFront) { if (onZoomPreview) { onZoomPreview(entry); } else if (onOpenPreview) { onOpenPreview(entry.id); } else if (onItemActivate) { onItemActivate(entry.id); } } else if (onItemActivate) { onItemActivate(entry.id); } }} onKeyDown={(event) => { if (!onItemActivate && !onOpenPreview && !onZoomPreview) return; if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); event.stopPropagation(); if (isFront) { if (onZoomPreview) { onZoomPreview(entry); } else if (onOpenPreview) { onOpenPreview(entry.id); } else { onItemActivate?.(entry.id); } } else { onItemActivate?.(entry.id); } } }} />
); })}
); }; const DetailPanel = ({ selectedDocuments = [], tags = [], tagLookupById = new Map(), onTagAdd, onTagRemove, onRegenerateThumbnails, onOpenPreview, onBulkTagAdd, onBulkTagRemove, onBulkReanalyze, onBulkCorrespondentAdd, onBulkCorrespondentRemove, onPromoteSelection, onUpdateTitle = async () => false, ensureAssetUrl = null, getDocumentAsset = () => null, ensurePreviewData = () => Promise.resolve(), correspondents = [], onCorrespondentAdd, onCorrespondentRemove, documentTypes = [], onDocumentTypeSet, onDocumentTypeClear, onBulkDocumentTypeSet, onBulkDocumentTypeClear, resolveApiPath, onFolderNavigate = null, resolveFolderPath = null, onClose = () => {}, }) => { const selectedCount = selectedDocuments.length; const singleDoc = selectedCount === 1 ? selectedDocuments[0] : null; const singleDocId = singleDoc?.id || null; const selectionKey = useMemo( () => selectedDocuments.map((doc) => doc?.id ?? '').join('|'), [selectedDocuments], ); const { downloadHref: singleDownloadHref, hasOcr: singleHasOcr, openOcr } = useMemo( () => createDocumentActionState({ document: singleDoc, resolveApiPath, ensurePreviewData, ensureAssetUrl, getDocumentAsset, }), [singleDoc, resolveApiPath, ensurePreviewData, ensureAssetUrl, getDocumentAsset], ); const detailSummary = useMemo(() => describeDocumentSummary(singleDoc), [singleDoc]); const headerTitle = useMemo(() => { if (selectedCount === 0) { return 'Document details'; } if (selectedCount === 1) { return detailSummary.title; } return `${selectedCount} document${selectedCount === 1 ? '' : 's'}`; }, [selectedCount, detailSummary]); const [titleEditDocId, setTitleEditDocId] = useState(null); const [titleDraft, setTitleDraft] = useState(''); const [titleSaving, setTitleSaving] = useState(false); const [titleError, setTitleError] = useState(null); const [zoomedPreview, setZoomedPreview] = useState(null); const bulkDocumentIds = useMemo( () => selectedDocuments.map((doc) => doc?.id).filter(Boolean), [selectedDocuments], ); useEffect(() => { if (!singleDoc) { setTitleEditDocId(null); setTitleDraft(''); setTitleError(null); setTitleSaving(false); return; } if (titleEditDocId && titleEditDocId !== singleDoc.id) { setTitleEditDocId(null); setTitleDraft(''); setTitleError(null); setTitleSaving(false); } }, [singleDoc, titleEditDocId]); useEffect(() => { setZoomedPreview(null); }, [selectionKey]); const startTitleEdit = useCallback(() => { if (!singleDoc) return; setTitleEditDocId(singleDoc.id); setTitleDraft(singleDoc.title || singleDoc.original_name); setTitleError(null); }, [singleDoc]); const cancelTitleEdit = useCallback(() => { setTitleEditDocId(null); setTitleDraft(''); setTitleError(null); setTitleSaving(false); }, []); const submitTitleEdit = useCallback( async (event) => { event.preventDefault(); if (!singleDoc) return; const trimmed = titleDraft.trim(); if (!trimmed) { setTitleError('Title cannot be empty.'); return; } setTitleSaving(true); try { const ok = await onUpdateTitle(singleDoc.id, trimmed); if (ok) { setTitleEditDocId(null); setTitleDraft(''); setTitleError(null); } else { setTitleError('Failed to update title.'); } } finally { setTitleSaving(false); } }, [singleDoc, titleDraft, onUpdateTitle], ); const handlePreviewActivate = useCallback( (docId) => { if (!docId) return; onPromoteSelection?.(docId); }, [onPromoteSelection], ); const singlePreviewNavigator = useAssetNavigator({ document: singleDoc, assetType: 'preview', ensureAssetUrl, getAsset: getDocumentAsset, prefetch: 3, }); const makePreviewItem = useCallback( (doc, ordinal = 1) => { if (!doc) return null; const asset = getDocumentAsset(doc, 'preview'); const assetView = createAssetView(asset); const object = assetView.getObject(ordinal); let url = object?.url || null; if (!url) { url = resolveDocumentAssetUrl(doc, 'preview', { ensureAssetUrl, getAsset: getDocumentAsset, ensureOptions: { start: ordinal, limit: 1 }, objectOrdinal: ordinal, }); } if (!url) { return null; } const metadata = object?.metadata || assetView.getPrimaryMetadata() || {}; const orientation = derivePreviewOrientation(metadata); return { id: doc.id, url, orientation, alt: doc.title, }; }, [ensureAssetUrl, getDocumentAsset], ); const stackDocuments = useMemo(() => { if (!selectedDocuments.length) return []; const seen = new Set(); const ordered = []; for (let index = selectedDocuments.length - 1; index >= 0; index -= 1) { const doc = selectedDocuments[index]; if (!doc?.id || seen.has(doc.id)) continue; seen.add(doc.id); ordered.push(doc); if (ordered.length >= MAX_PREVIEW_STACK_ITEMS) { break; } } return ordered; }, [selectedDocuments]); const stackTopDocument = stackDocuments[0] || null; const stackTopDocId = stackTopDocument?.id || null; const stackPreviewNavigator = useAssetNavigator({ document: stackTopDocument, assetType: 'preview', ensureAssetUrl, getAsset: getDocumentAsset, prefetch: 3, }); const singlePreviewItems = useMemo(() => { if (!singleDoc) return []; const url = singlePreviewNavigator.currentUrl; if (!url) { return []; } const orientation = derivePreviewOrientation(singlePreviewNavigator.currentMetadata); return [ { id: singleDoc.id, url, orientation, alt: singleDoc.title, }, ]; }, [singleDoc, singlePreviewNavigator.currentUrl, singlePreviewNavigator.currentMetadata]); const stackPreviews = useMemo(() => { if (!stackDocuments.length) { return []; } return stackDocuments .map((doc) => { if (!doc) return null; if (stackTopDocument && doc.id === stackTopDocument.id) { const url = stackPreviewNavigator.currentUrl; if (!url) { return null; } const orientation = derivePreviewOrientation(stackPreviewNavigator.currentMetadata); return { id: doc.id, url, orientation, alt: doc.title, }; } return makePreviewItem(doc, 1); }) .filter(Boolean); }, [ stackDocuments, stackTopDocument, stackPreviewNavigator.currentUrl, stackPreviewNavigator.currentMetadata, makePreviewItem, ]); const singleCardinality = singlePreviewNavigator.cardinality; const singleEffectiveCardinality = singleCardinality || (singlePreviewNavigator.currentUrl ? 1 : 0); const singleHasPreview = Boolean(singlePreviewNavigator.currentUrl); const topDocId = stackTopDocument?.id || null; const topCardinality = stackPreviewNavigator.cardinality; const topEffectiveCardinality = topCardinality || (stackPreviewNavigator.currentUrl ? 1 : 0); const topHasPreview = Boolean(stackPreviewNavigator.currentUrl); const bulkTagUnion = useMemo(() => { if (!selectedDocuments.length) return []; const tagMap = new Map(); selectedDocuments.forEach((doc) => { (doc.tags || []).forEach((tag) => { if (!tag?.label) return; const label = tag.label.trim(); if (!label) return; if (!tagMap.has(label)) { const fallback = tagLookupById.get(tag.id); tagMap.set(label, { id: tag.id, label, color: tag.color ?? fallback?.color ?? null, }); } }); }); return [...tagMap.values()].sort((a, b) => a.label.localeCompare(b.label)); }, [selectedDocuments, tagLookupById]); const stackTotalSizeBytes = useMemo(() => { if (!stackPreviews.length) return 0; const byId = new Map(selectedDocuments.map((doc) => [doc.id, doc])); return stackPreviews.reduce((sum, item) => { const source = byId.get(item.id); const bytes = source?.current_version?.size_bytes; return sum + (typeof bytes === 'number' ? bytes : 0); }, 0); }, [stackPreviews, selectedDocuments]); const availableCorrespondents = useMemo( () => (Array.isArray(correspondents) ? correspondents : []), [correspondents], ); const correspondentOptions = useMemo(() => { const seen = new Set(); return availableCorrespondents.reduce((options, entry) => { if (typeof entry?.name !== 'string') { return options; } const name = entry.name.trim(); if (!name) { return options; } const lower = name.toLowerCase(); if (seen.has(lower)) { return options; } seen.add(lower); options.push(name); return options; }, []); }, [availableCorrespondents]); const documentTypeOptions = useMemo(() => { const seen = new Set(); return (documentTypes || []).reduce((options, entry) => { if (typeof entry?.name !== 'string') { return options; } const name = entry.name.trim(); if (!name) { return options; } const lower = name.toLowerCase(); if (seen.has(lower)) { return options; } seen.add(lower); options.push(name); return options; }, []); }, [documentTypes]); const singleDocumentType = useMemo(() => resolveDocumentType(singleDoc), [singleDoc]); const singleCorrespondents = useMemo(() => { if (!singleDoc) return []; return sortCorrespondents(singleDoc.correspondents || []); }, [singleDoc]); const singleFolderPath = useMemo(() => { if (!singleDoc?.folder_id) { return null; } if (typeof resolveFolderPath !== 'function') { return null; } const segments = resolveFolderPath(singleDoc.folder_id); if (!Array.isArray(segments) || !segments.some((segment) => segment?.id && segment.id !== 'root')) { return null; } return segments; }, [singleDoc?.folder_id, resolveFolderPath]); const folderLabel = detailSummary.folderLabel; const folderDisplayNode = useMemo(() => { if (!singleDoc) { return folderLabel || '—'; } if (!singleFolderPath?.length) { return folderLabel || '—'; } return ( {singleFolderPath.map((segment, index) => { const label = segment?.name || '…'; const targetId = segment?.id || null; const key = `${targetId || label}-${index}`; const isClickable = Boolean(targetId) && typeof onFolderNavigate === 'function'; const href = !isClickable ? null : targetId === 'root' ? '/documents' : `/documents/folder/${targetId}`; return ( {index > 0 ? / : null} {isClickable ? ( { if ( event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey ) { return; } event.preventDefault(); event.stopPropagation(); onFolderNavigate(targetId); }} > {label} ) : ( {label} )} ); })} ); }, [singleDoc, singleFolderPath, folderLabel, onFolderNavigate]); const detailInfoRows = useMemo(() => { if (!singleDoc) { return []; } const allowedKeys = new Set(['uploaded', 'size', 'type', 'issued', 'pages', 'created', 'updated', 'folder']); const rows = detailSummary.summaryRows .filter((row) => { if (!allowedKeys.has(row.key)) { return false; } if (row.key === 'pages') { return Number.isFinite(detailSummary.pageCount); } if (row.key === 'folder') { return Boolean(singleFolderPath?.length); } return true; }) .map((row) => (row.key === 'folder' ? { ...row, value: folderDisplayNode } : row)); rows.push({ key: 'original-name', label: 'Original filename', value: singleDoc.original_name || '—', }); return rows; }, [singleDoc, detailSummary, folderDisplayNode, singleFolderPath]); const bulkCorrespondents = useMemo(() => { if (selectedDocuments.length <= 1) { const doc = selectedDocuments[0]; return doc ? sortCorrespondents(doc.correspondents || []) : []; } const map = new Map(); selectedDocuments.forEach((doc) => { if (!doc?.id) return; (doc.correspondents || []).forEach((entry) => { if (!entry?.id || typeof entry.name !== 'string') return; if (!map.has(entry.id)) { map.set(entry.id, { id: entry.id, name: entry.name, documentIds: new Set(), }); } map.get(entry.id).documentIds.add(doc.id); }); }); return [...map.values()] .map((entry) => ({ id: entry.id, name: entry.name, documentIds: [...entry.documentIds], count: entry.documentIds.size, })) .sort((a, b) => a.name.localeCompare(b.name)); }, [selectedDocuments]); const bulkDocumentTypes = useMemo(() => { if (!selectedDocuments.length) { return []; } const map = new Map(); selectedDocuments.forEach((doc) => { const type = resolveDocumentType(doc); if (!type) return; const key = type.id ?? type.name.toLowerCase(); if (!map.has(key)) { map.set(key, { id: type.id ?? null, name: type.name, count: 0 }); } map.get(key).count += 1; }); return [...map.values()].sort((a, b) => a.name.localeCompare(b.name)); }, [selectedDocuments]); const bulkDocumentTypeSummary = useMemo(() => { if (!bulkDocumentTypes.length) { return ''; } return bulkDocumentTypes .map((entry) => { if (!entry?.name) { return null; } const suffix = entry.count === selectedDocuments.length ? '' : ` (${entry.count})`; return `${entry.name}${suffix}`; }) .filter(Boolean) .join(', '); }, [bulkDocumentTypes, selectedDocuments.length]); const handleBulkCorrespondentRemove = useCallback( (entry) => { if (!entry?.id) return; if (onBulkCorrespondentRemove) { return onBulkCorrespondentRemove({ assignments: [ { correspondent_id: entry.id, }, ], documentIds: entry.documentIds, }); } if (!onCorrespondentRemove) return; const targets = entry.documentIds && entry.documentIds.length ? entry.documentIds : selectedDocuments .filter((doc) => (doc.correspondents || []).some((item) => item.id === entry.id)) .map((doc) => doc.id); return Promise.all( targets.map((documentId) => onCorrespondentRemove({ documentId, correspondentId: entry.id, }), ), ).catch(() => {}); }, [onBulkCorrespondentRemove, onCorrespondentRemove, selectedDocuments], ); const openZoomPreview = useCallback((config) => { if (!config) return; setZoomedPreview({ mode: config.mode, docId: config.docId ?? null, }); }, []); const closeZoomPreview = useCallback(() => { setZoomedPreview(null); }, []); const handleSingleZoom = useCallback( (entry) => { if (!singleHasPreview) return; const targetId = entry?.id ?? singleDocId; if (!targetId) return; openZoomPreview({ mode: 'single', docId: targetId }); }, [openZoomPreview, singleHasPreview, singleDocId], ); const handleStackZoom = useCallback( (entry) => { if (!stackTopDocId || entry?.id !== stackTopDocId) return; if (!topHasPreview) return; openZoomPreview({ mode: 'stack', docId: stackTopDocId }); }, [openZoomPreview, stackTopDocId, topHasPreview], ); const zoomDisplay = useMemo(() => { if (!zoomedPreview) { return null; } if ( zoomedPreview.mode === 'single' && singleDocId && singleDoc && singleHasPreview && zoomedPreview.docId === singleDocId ) { return { url: singlePreviewNavigator.currentUrl, alt: singleDoc.title, canGoPrev: singleEffectiveCardinality > 1 && Boolean(singlePreviewNavigator.canGoPrev), canGoNext: singleEffectiveCardinality > 1 && Boolean(singlePreviewNavigator.canGoNext), goPrev: singlePreviewNavigator.goPrev, goNext: singlePreviewNavigator.goNext, }; } if ( zoomedPreview.mode === 'stack' && stackTopDocId && stackTopDocument && zoomedPreview.docId === stackTopDocId && topHasPreview ) { return { url: stackPreviewNavigator.currentUrl, alt: stackTopDocument.title, canGoPrev: topEffectiveCardinality > 1 && Boolean(stackPreviewNavigator.canGoPrev), canGoNext: topEffectiveCardinality > 1 && Boolean(stackPreviewNavigator.canGoNext), goPrev: stackPreviewNavigator.goPrev, goNext: stackPreviewNavigator.goNext, }; } return null; }, [ zoomedPreview, singleDoc, singleDocId, singleHasPreview, singlePreviewNavigator.currentUrl, singlePreviewNavigator.canGoPrev, singlePreviewNavigator.canGoNext, singlePreviewNavigator.goPrev, singlePreviewNavigator.goNext, singleEffectiveCardinality, stackTopDocument, stackTopDocId, topHasPreview, stackPreviewNavigator.currentUrl, stackPreviewNavigator.canGoPrev, stackPreviewNavigator.canGoNext, stackPreviewNavigator.goPrev, stackPreviewNavigator.goNext, topEffectiveCardinality, ]); useEffect(() => { if (zoomedPreview && !zoomDisplay) { setZoomedPreview(null); } }, [zoomedPreview, zoomDisplay]); const { documentId: singleNavigatorDocId, asset: singleNavigatorAsset, ordinal: singleNavigatorOrdinal, canGoPrev: singleNavigatorCanGoPrev, canGoNext: singleNavigatorCanGoNext, cardinality: singleNavigatorCardinality, } = singlePreviewNavigator; const { documentId: stackNavigatorDocId, asset: stackNavigatorAsset, ordinal: stackNavigatorOrdinal, canGoPrev: stackNavigatorCanGoPrev, canGoNext: stackNavigatorCanGoNext, cardinality: stackNavigatorCardinality, } = stackPreviewNavigator; useEffect(() => { if (typeof ensureAssetUrl !== 'function') { return; } const warmNavigator = (navigator) => { const { documentId, asset, ordinal, canGoPrev, canGoNext, cardinality, } = navigator; if (!documentId || !asset || !Number.isFinite(ordinal)) { return; } const requests = []; if (canGoPrev) { const prevOrdinal = Math.max(1, ordinal - 1); if (!cardinality || prevOrdinal <= cardinality) { requests.push( ensureAssetUrl(documentId, asset, { start: prevOrdinal, limit: 1, objectOrdinal: prevOrdinal, }), ); } } if (canGoNext) { const nextOrdinal = ordinal + 1; if (!cardinality || nextOrdinal <= cardinality) { requests.push( ensureAssetUrl(documentId, asset, { start: nextOrdinal, limit: 1, objectOrdinal: nextOrdinal, }), ); } } requests.forEach((promise) => promise?.catch?.(() => {})); }; const singleWarmState = { documentId: singleNavigatorDocId, asset: singleNavigatorAsset, ordinal: singleNavigatorOrdinal, canGoPrev: singleNavigatorCanGoPrev, canGoNext: singleNavigatorCanGoNext, cardinality: singleNavigatorCardinality, }; const stackWarmState = { documentId: stackNavigatorDocId, asset: stackNavigatorAsset, ordinal: stackNavigatorOrdinal, canGoPrev: stackNavigatorCanGoPrev, canGoNext: stackNavigatorCanGoNext, cardinality: stackNavigatorCardinality, }; warmNavigator(singleWarmState); warmNavigator(stackWarmState); }, [ ensureAssetUrl, singleNavigatorDocId, singleNavigatorAsset, singleNavigatorOrdinal, singleNavigatorCanGoPrev, singleNavigatorCanGoNext, singleNavigatorCardinality, stackNavigatorDocId, stackNavigatorAsset, stackNavigatorOrdinal, stackNavigatorCanGoPrev, stackNavigatorCanGoNext, stackNavigatorCardinality, ]); const renderSingle = () => { if (!singleDoc) { return

Select a document to view metadata, tags and actions.

; } const displayName = singleDoc.title || singleDoc.original_name; const isEditingTitle = titleEditDocId === singleDoc.id; const tagsForDoc = Array.isArray(singleDoc.tags) ? singleDoc.tags : []; const metadata = singleDoc.metadata && Object.keys(singleDoc.metadata).length > 0 ? singleDoc.metadata : null; const effectiveCardinality = singleEffectiveCardinality; const canGoPrev = singlePreviewNavigator.canGoPrev; const canGoNext = singlePreviewNavigator.canGoNext; const hasPreviewImage = singleHasPreview; const interceptNavPointer = (event) => { event.preventDefault(); event.stopPropagation(); }; return ( <>
{hasPreviewImage && (effectiveCardinality > 1 || canGoPrev || canGoNext) ? (
) : null}
{isEditingTitle ? (
{ setTitleDraft(event.target.value); if (titleError) { setTitleError(null); } }} onKeyDown={(event) => { if (event.key === 'Escape') { event.preventDefault(); cancelTitleEdit(); } }} aria-label="Document title" autoFocus />
) : ( <>

{displayName}

)}
{titleError ?
{titleError}
: null}
{detailInfoRows.map((row) => { const rawValue = row.value; const displayValue = rawValue === null || rawValue === undefined || rawValue === '' ? '—' : rawValue; return (
{row.label}:{' '} {displayValue}
); })}
Document type
{singleDocumentType?.name ? ( {singleDocumentType.name} ) : ( None assigned. )} {singleDocumentType && onDocumentTypeClear ? ( ) : null}
{onDocumentTypeSet ? (
{ event.preventDefault(); const form = event.currentTarget; const input = form.elements.documentType; const value = input?.value?.trim(); if (!value) { return; } onDocumentTypeSet?.({ document: singleDoc, name: value, input }); }} > {documentTypeOptions.map((name) => (
) : null}
({ id: tag.id, label: tag.label, color: tag.color || tagLookupById.get(tag.id)?.color, }))} onRemove={(tag) => onTagRemove(singleDoc.id, tag.id)} onAdd={({ value, input }) => onTagAdd(singleDoc, value, input)} datalistId="tag-catalog-single" datalistOptions={tags} /> onCorrespondentRemove?.({ documentId: singleDoc.id, correspondentId: entry.id, }) } onAdd={({ name, input }) => onCorrespondentAdd?.({ document: singleDoc, name, input, }) } datalistId="correspondent-catalog-single" datalistOptions={correspondentOptions} /> {metadata && (
Metadata
{JSON.stringify(metadata, null, 2)}
)} ); }; const renderBulk = () => { const countLabel = `${selectedCount} document${selectedCount === 1 ? '' : 's'}`; const sizeLabel = stackTotalSizeBytes ? formatFileSize(stackTotalSizeBytes) : '—'; const topDocIdLocal = topDocId; const topCardinalityLocal = topEffectiveCardinality; const topHasPreview = Boolean(stackPreviewNavigator.currentUrl); const topCanGoPrev = stackPreviewNavigator.canGoPrev; const topCanGoNext = stackPreviewNavigator.canGoNext; const interceptTopNavPointer = (event) => { event.preventDefault(); event.stopPropagation(); }; const documentIds = bulkDocumentIds; return ( <>
{topDocIdLocal && topHasPreview && (topCardinalityLocal > 1 || topCanGoPrev || topCanGoNext) ? (
) : null}

{countLabel}

Total size (stack): {sizeLabel}
onBulkTagRemove?.({ label: tag.label, documentIds })} onAdd={({ value, input }) => onBulkTagAdd?.({ label: value, input, documentIds }) } addPlaceholder="Add tag to selection" addButtonLabel="Add tag" datalistId="tag-catalog-bulk" datalistOptions={tags} className="bulk-tags" />
Document type
{bulkDocumentTypeSummary || 'None assigned.'} {onBulkDocumentTypeClear && bulkDocumentTypes.length > 0 ? ( ) : null}
{onBulkDocumentTypeSet ? (
{ event.preventDefault(); const form = event.currentTarget; const input = form.elements.documentType; const value = input?.value?.trim(); if (!value) { return; } onBulkDocumentTypeSet?.({ name: value, input, documentIds }); }} > {documentTypeOptions.map((name) => (
) : null}
onBulkCorrespondentAdd?.({ name, input, documentIds }) } addPlaceholder="Add correspondent to selection" datalistId="correspondent-catalog-bulk" datalistOptions={correspondentOptions} showCount className="bulk-correspondents" /> ); }; const isBulkSelection = selectedCount > 1; const showOcrAction = Boolean(singleDoc && singleHasOcr); return ( <> ); }; export default DetailPanel;