introduce a unified document viewer component.

This commit is contained in:
2025-12-04 02:21:11 +01:00
parent cbd463e1d9
commit f57bb9f6eb
6 changed files with 82 additions and 86 deletions
+3 -3
View File
@@ -116,8 +116,8 @@ export const useWorkspaceSurface = ({
<DocumentViewerPanel <DocumentViewerPanel
variant="sidebar" variant="sidebar"
sidebarMode={sidebarMode} sidebarMode={sidebarMode}
onClosePanel={onClose} onClose={onClose}
onMaximizePanel={onOpenPreview} onMaximize={onOpenPreview}
tagOptions={tagOptions} tagOptions={tagOptions}
{...restDetailProps} {...restDetailProps}
/> />
@@ -194,7 +194,7 @@ export const useWorkspaceSurface = ({
ensurePreviewData={ensurePreviewData} ensurePreviewData={ensurePreviewData}
notifyApiError={notifyApiError} notifyApiError={notifyApiError}
sidebarToggle={sidebarToggle} sidebarToggle={sidebarToggle}
onClosePanel={closeDocumentPreview} onClose={closeDocumentPreview}
resolveFolderPath={resolveFolderPath} resolveFolderPath={resolveFolderPath}
/> />
); );
+5 -30
View File
@@ -1,7 +1,6 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import PdfViewer from '../preview/PdfViewer'; import UnifiedDocumentViewer from '../preview/UnifiedDocumentViewer';
import MediaViewer from '../preview/MediaViewer';
import PanelHeader from '../ui/PanelHeader'; import PanelHeader from '../ui/PanelHeader';
import { IconX, DownloadIcon, FileInfoIcon } from '../ui/icons'; import { IconX, DownloadIcon, FileInfoIcon } from '../ui/icons';
@@ -14,16 +13,6 @@ interface PreviewZoomOverlayProps {
document?: Document | null; document?: Document | null;
} }
type DisplayKind = 'image' | 'pdf';
const determineDisplayKind = (doc?: Document | null): DisplayKind => {
const type = doc?.mime_type?.toLowerCase?.() || '';
if (type.includes('pdf')) {
return 'pdf';
}
return 'image';
};
const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
open = false, open = false,
onClose, onClose,
@@ -41,7 +30,6 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
} }
const downloadUrl = lastDocumentRef.current?.current_version?.download?.url; const downloadUrl = lastDocumentRef.current?.current_version?.download?.url;
const displayKind = determineDisplayKind(lastDocumentRef.current);
const documentTitle = lastDocumentRef.current?.title || undefined; const documentTitle = lastDocumentRef.current?.title || undefined;
useEffect(() => { useEffect(() => {
@@ -172,23 +160,10 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
ref={scrollRef} ref={scrollRef}
tabIndex={-1} tabIndex={-1}
> >
{displayKind === 'pdf' ? ( <UnifiedDocumentViewer
<PdfViewer document={lastDocumentRef.current}
src={downloadUrl} viewportRef={scrollRef}
title={documentTitle} />
className="preview-zoom__pdf-viewer"
viewportRef={scrollRef}
/>
) : (
<MediaViewer
src={downloadUrl}
alt={documentTitle}
className="preview-zoom__image"
draggable={false}
onClick={(e) => e.stopPropagation()}
mimeType={lastDocumentRef.current?.mime_type || undefined}
/>
)}
</div> </div>
</div> </div>
</div> </div>
+7 -40
View File
@@ -1,8 +1,7 @@
import { useCallback, useMemo, useRef } from 'react'; import { useCallback, useMemo, useRef } from 'react';
import type { JSX } from 'react'; import type { JSX } from 'react';
import DocumentInfoPanel from '../documents/DocumentInfoPanel'; import DocumentInfoPanel from '../documents/DocumentInfoPanel';
import PdfViewer from './PdfViewer'; import UnifiedDocumentViewer from './UnifiedDocumentViewer';
import MediaViewer from './MediaViewer';
import type { Document } from '../types/documents'; import type { Document } from '../types/documents';
@@ -33,7 +32,6 @@ interface DocumentViewerLayoutProps {
previewLoadingMessage?: string; previewLoadingMessage?: string;
layoutMode?: LayoutMode; layoutMode?: LayoutMode;
} }
const DocumentViewerLayout = ({ const DocumentViewerLayout = ({
document, document,
summaryProps = {}, summaryProps = {},
@@ -49,43 +47,12 @@ const DocumentViewerLayout = ({
const isStacked = layoutMode === 'stacked'; const isStacked = layoutMode === 'stacked';
const viewportRef = useRef<HTMLDivElement>(null); const viewportRef = useRef<HTMLDivElement>(null);
const previewContent = useMemo(() => { const previewContent = useMemo(() => (
if (!document) { <UnifiedDocumentViewer
return null; document={document}
} viewportRef={viewportRef}
/>
const downloadUrl = document.current_version?.download?.url; ), [document, viewportRef]);
if (!downloadUrl) {
return null;
}
const normalizedMimeType = (document.mime_type || '').toLowerCase();
const normalizedFilename = document.filename;
const isPdf = normalizedMimeType === 'application/pdf'
|| normalizedMimeType === 'application/x-pdf';
if (isPdf) {
const documentTitle = document.title;
return (
<PdfViewer
src={downloadUrl}
title={documentTitle ? `Preview of ${documentTitle}` : undefined}
viewportRef={viewportRef}
/>
);
}
return (
<MediaViewer
src={downloadUrl}
mimeType={normalizedMimeType}
filename={normalizedFilename}
alt={`Preview of ${document.title}`}
/>
);
}, [document, viewportRef]);
const renderViewportPane = useCallback(() => ( const renderViewportPane = useCallback(() => (
<div className="document-viewer__viewport" ref={viewportRef}> <div className="document-viewer__viewport" ref={viewportRef}>
+8 -8
View File
@@ -39,10 +39,10 @@ interface DocumentViewerPanelProps extends DocumentSummarySectionProps {
ensurePreviewData?: (docId: DocumentId, options?: { signal?: AbortSignal }) => Promise<Document | null>; ensurePreviewData?: (docId: DocumentId, options?: { signal?: AbortSignal }) => Promise<Document | null>;
notifyApiError?: (error: unknown, fallbackMessage?: string) => void; notifyApiError?: (error: unknown, fallbackMessage?: string) => void;
sidebarToggle?: ReactNode; sidebarToggle?: ReactNode;
onClosePanel?: () => void; onClose?: () => void;
resolveFolderPath?: (doc: Document | null) => Array<{ id?: string; name?: string }>; resolveFolderPath?: (doc: Document | null) => Array<{ id?: string; name?: string }>;
variant?: 'viewer' | 'sidebar'; variant?: 'viewer' | 'sidebar';
onMaximizePanel?: (args: { documentIds: Array<string> }) => void; onMaximize?: (args: { documentIds: Array<string> }) => void;
sidebarMode?: SidebarMode; sidebarMode?: SidebarMode;
} }
@@ -95,10 +95,10 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
ensurePreviewData, ensurePreviewData,
notifyApiError, notifyApiError,
sidebarToggle = null, sidebarToggle = null,
onClosePanel, onClose,
resolveFolderPath, resolveFolderPath,
variant = 'viewer', variant = 'viewer',
onMaximizePanel, onMaximize,
sidebarMode = 'overlay', sidebarMode = 'overlay',
}) => { }) => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -299,7 +299,7 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
canZoom: Boolean(document), canZoom: Boolean(document),
}); });
const maximizeButton = isSidebarVariant && onMaximizePanel const maximizeButton = isSidebarVariant && onMaximize
? ( ? (
<button <button
type="button" type="button"
@@ -310,7 +310,7 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
if (targetId == null) { if (targetId == null) {
return; return;
} }
onMaximizePanel?.({ documentIds: [targetId] }); onMaximize?.({ documentIds: [targetId] });
}} }}
aria-label="Maximize" aria-label="Maximize"
title="Maximize" title="Maximize"
@@ -320,12 +320,12 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
) )
: null; : null;
const closeButton = onClosePanel const closeButton = onClose
? ( ? (
<button <button
type="button" type="button"
className="icon-button" className="icon-button"
onClick={() => onClosePanel?.()} onClick={() => onClose?.()}
aria-label="Close preview" aria-label="Close preview"
title="Close preview" title="Close preview"
> >
@@ -0,0 +1,57 @@
import React, { useMemo } from 'react';
import PdfViewer from './PdfViewer';
import MediaViewer from './MediaViewer';
import type { Document } from '../types/documents';
interface UnifiedDocumentViewerProps {
document: Document | null;
viewportRef?: React.RefObject<HTMLDivElement>;
}
const UnifiedDocumentViewer: React.FC<UnifiedDocumentViewerProps> = ({
document,
viewportRef,
}) => {
const content = useMemo(() => {
if (!document) {
return null;
}
const downloadUrl = document.current_version?.download?.url;
if (!downloadUrl) {
return null;
}
const normalizedMimeType = (document.mime_type || '').toLowerCase();
const normalizedFilename = document.filename;
const isPdf = normalizedMimeType === 'application/pdf'
|| normalizedMimeType === 'application/x-pdf';
if (isPdf) {
return (
<PdfViewer
src={downloadUrl}
title={document.title}
viewportRef={viewportRef}
/>
);
}
return (
<MediaViewer
src={downloadUrl}
mimeType={normalizedMimeType}
filename={normalizedFilename}
alt={document.title}
/>
);
}, [
document,
viewportRef,
]);
return content;
};
export default UnifiedDocumentViewer;
+2 -5
View File
@@ -32,7 +32,7 @@
--header-clearance: calc(2.5rem + 0.5rem); --header-clearance: calc(2.5rem + 0.5rem);
} }
.preview-zoom__image { .preview-zoom__scroll .document-viewer__object--image {
width: auto; width: auto;
height: auto; height: auto;
box-shadow: 0 32px 120px var(--shadow-deep); box-shadow: 0 32px 120px var(--shadow-deep);
@@ -59,10 +59,7 @@
outline: none; outline: none;
} }
.preview-zoom__scroll .document-viewer__object--pdf {
.preview-zoom__pdf-viewer {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }