From e46318aee58014b03c3c2e93a1729fc69b7c115b Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Wed, 3 Dec 2025 17:07:51 +0100 Subject: [PATCH] feat: Add OCR script for PDFs using Ollama and refine document preview navigation and zoom behavior. --- frontend/src/app/DocumentsRoute.tsx | 9 ++- frontend/src/detail/PreviewZoomOverlay.tsx | 74 ++++++++++++++------ frontend/src/preview/PdfViewer.tsx | 3 +- frontend/src/preview/PreviewContext.tsx | 11 ++- frontend/src/styles/detail/detail-panels.css | 22 ++++++ frontend/src/styles/preview/preview-zoom.css | 34 ++++++++- 6 files changed, 126 insertions(+), 27 deletions(-) diff --git a/frontend/src/app/DocumentsRoute.tsx b/frontend/src/app/DocumentsRoute.tsx index fc8ae29..9f5cf76 100644 --- a/frontend/src/app/DocumentsRoute.tsx +++ b/frontend/src/app/DocumentsRoute.tsx @@ -35,6 +35,10 @@ const DocumentsRouteContent: React.FC = () => { navigate(target); }, [navigate]); + const handleDocumentNavigate = useCallback((documentId: string) => { + navigate(`/documents/${documentId}`); + }, [navigate]); + const documentsTablePropsWithNav = useMemo(() => ( surfaceConfig.documentsTableProps ? { ...surfaceConfig.documentsTableProps, onBreadcrumbNavigate: handleHeaderBreadcrumbClick } @@ -76,7 +80,10 @@ const DocumentsRouteContent: React.FC = () => { const content = renderSurface(); return ( - + {content} diff --git a/frontend/src/detail/PreviewZoomOverlay.tsx b/frontend/src/detail/PreviewZoomOverlay.tsx index 8211539..d7a5673 100644 --- a/frontend/src/detail/PreviewZoomOverlay.tsx +++ b/frontend/src/detail/PreviewZoomOverlay.tsx @@ -3,12 +3,15 @@ import { createPortal } from 'react-dom'; import { clamp } from '../utils/math'; import PdfViewer from '../preview/PdfViewer'; import MediaViewer from '../preview/MediaViewer'; +import PanelHeader from '../ui/PanelHeader'; +import { IconX, DownloadIcon, FileInfoIcon } from '../ui/icons'; import type { Document } from '../types/documents'; interface PreviewZoomOverlayProps { open?: boolean; onClose?: () => void; + onMaximize?: () => void; document?: Document | null; } @@ -44,6 +47,7 @@ const noop = () => { }; const PreviewZoomOverlay: React.FC = ({ open = false, onClose = noop, + onMaximize, document: overlayDocument = null, }) => { const portalTarget = document.body; @@ -235,25 +239,10 @@ const PreviewZoomOverlay: React.FC = ({ }; - const toggleZoomAtPoint = (clientX: number, clientY: number) => { - if (isPdfDisplay) { - return; - } - const media = mediaRef.current; - setIsNativeScale((current) => { - if (!current && media) { - const rect = media.getBoundingClientRect(); - const xRatio = rect.width > 0 ? (clientX - rect.left) / rect.width : 0.5; - const yRatio = rect.height > 0 ? (clientY - rect.top) / rect.height : 0.5; - focusRef.current = { - xRatio: clamp(xRatio, 0, 1), - yRatio: clamp(yRatio, 0, 1), - }; - } else { - focusRef.current = null; - } - return !current; - }); + const toggleZoomAtPoint = (_clientX: number, _clientY: number) => { + // Zooming is disabled for images as per user request. + // PDFs handle their own zooming via the PdfViewer. + return; }; const handleContentClick = (event: React.MouseEvent) => { @@ -287,7 +276,7 @@ const PreviewZoomOverlay: React.FC = ({ const contentStyle: CSSProperties = isNativeScale ? { - cursor: 'zoom-out', + cursor: 'default', width: naturalSize.width ? `${naturalSize.width}px` : 'auto', height: naturalSize.height ? `${naturalSize.height}px` : 'auto', maxWidth: 'none', @@ -295,7 +284,7 @@ const PreviewZoomOverlay: React.FC = ({ touchAction: 'manipulation', } : { - cursor: 'zoom-in', + cursor: 'default', maxWidth: 'calc(100vw - 2 * var(--preview-padding, 1.5vmin))', maxHeight: 'calc(100vh - 2 * var(--preview-padding, 1.5vmin))', touchAction: 'manipulation', @@ -307,6 +296,8 @@ const PreviewZoomOverlay: React.FC = ({ const effectiveDisplay = documentLink; + const downloadUrl = activeDocument?.current_version?.download?.url; + return createPortal( (
= ({ onClick={onClose} onKeyDown={handleKeyDown} > + + + + } + actions={ + <> + {downloadUrl && ( + + + + )} + {onMaximize && ( + + )} + + } + />
{ diff --git a/frontend/src/preview/PdfViewer.tsx b/frontend/src/preview/PdfViewer.tsx index a16c06b..97c7e81 100644 --- a/frontend/src/preview/PdfViewer.tsx +++ b/frontend/src/preview/PdfViewer.tsx @@ -379,7 +379,8 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX. const stackStyle = useMemo(() => ({ '--pdf-viewer-viewport-width': `${viewportWidth}px`, '--pdf-viewer-viewport-height': `${viewportHeight}px`, - }), [viewportHeight, viewportWidth]); + cursor: viewMode === 'fit-width' ? 'zoom-out' : 'zoom-in', + }), [viewportHeight, viewportWidth, viewMode]); const toggleViewMode = useCallback(() => { setViewMode((prev) => (prev === 'fit-width' ? 'contain' : 'fit-width')); diff --git a/frontend/src/preview/PreviewContext.tsx b/frontend/src/preview/PreviewContext.tsx index ba662e2..5408f75 100644 --- a/frontend/src/preview/PreviewContext.tsx +++ b/frontend/src/preview/PreviewContext.tsx @@ -21,9 +21,10 @@ export const usePreviewContext = () => { interface PreviewProviderProps { children: React.ReactNode; ensureDownloadUrl?: (docId: Identifier) => Promise; + onNavigate?: (documentId: Identifier) => void; } -export const PreviewProvider: React.FC = ({ children, ensureDownloadUrl }) => { +export const PreviewProvider: React.FC = ({ children, ensureDownloadUrl, onNavigate }) => { const [previewDoc, setPreviewDoc] = useState(null); const [previewUrl, setPreviewUrl] = useState(null); @@ -36,6 +37,13 @@ export const PreviewProvider: React.FC = ({ children, ensu setPreviewUrl(null); }, []); + const handleMaximize = useCallback(() => { + if (previewDoc && onNavigate) { + onNavigate(previewDoc.id); + closePreview(); + } + }, [previewDoc, onNavigate, closePreview]); + useEffect(() => { if (!previewDoc) { setPreviewUrl(null); @@ -87,6 +95,7 @@ export const PreviewProvider: React.FC = ({ children, ensu diff --git a/frontend/src/styles/detail/detail-panels.css b/frontend/src/styles/detail/detail-panels.css index 24fd5aa..dec6b3f 100644 --- a/frontend/src/styles/detail/detail-panels.css +++ b/frontend/src/styles/detail/detail-panels.css @@ -75,6 +75,28 @@ margin-left: auto; } +/* Dark theme variant for preview zoom overlay */ +.panel-header--dark { + background: var(--overlay-dark); + color: var(--text-on-dark); + pointer-events: auto; +} + +.panel-header--dark .icon-button, +.panel-header--dark button, +.panel-header--dark a.icon-button { + color: var(--text-on-dark); + opacity: 0.75; + pointer-events: auto; +} + +.panel-header--dark .icon-button:hover:not([disabled]), +.panel-header--dark button:hover:not([disabled]), +.panel-header--dark a.icon-button:hover { + background: var(--surface-hover); + opacity: 1; +} + .panel-body { padding: 0.5rem 1rem; } diff --git a/frontend/src/styles/preview/preview-zoom.css b/frontend/src/styles/preview/preview-zoom.css index 6b8f970..cb85e33 100644 --- a/frontend/src/styles/preview/preview-zoom.css +++ b/frontend/src/styles/preview/preview-zoom.css @@ -6,10 +6,12 @@ display: flex; align-items: center; justify-content: center; - z-index: 2000000; - cursor: zoom-out; + z-index: 6000000; + cursor: default; opacity: 0; pointer-events: none; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); } .preview-zoom-backdrop--visible { @@ -23,8 +25,9 @@ display: flex; align-items: center; justify-content: center; - z-index: 3000000; + z-index: 6000001; --preview-padding: 1.5vmin; + --header-clearance: calc(2.5rem + 0.5rem); } .preview-zoom__stage--pdf { @@ -42,6 +45,8 @@ display: flex; align-items: center; justify-content: center; + padding-top: var(--header-clearance); + box-sizing: border-box; } .preview-zoom__scroll--pdf { @@ -60,6 +65,8 @@ } .preview-zoom__scroll--native { + width: 100%; + height: 100%; overflow: auto; cursor: zoom-out; justify-content: flex-start; @@ -73,6 +80,8 @@ display: flex; justify-content: center; align-items: flex-start; + padding-top: var(--header-clearance); + box-sizing: border-box; } .preview-zoom__pdf-viewer { @@ -93,4 +102,23 @@ z-index: -1; pointer-events: none; border-radius: inherit; +} + +/* Header positioning for preview zoom overlay */ +.preview-zoom-backdrop .panel-header--dark { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 6000002; + cursor: default; + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + pointer-events: auto; + box-shadow: 0 1rem 0.5rem var(--shadow-subtle); +} + +/* Prevent header clicks from closing backdrop */ +.preview-zoom-backdrop .panel-header--dark * { + pointer-events: auto; } \ No newline at end of file