diff --git a/frontend/src/app/DocumentsRoute.tsx b/frontend/src/app/DocumentsRoute.tsx
index 70ba486..fc8ae29 100644
--- a/frontend/src/app/DocumentsRoute.tsx
+++ b/frontend/src/app/DocumentsRoute.tsx
@@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom';
import {
DocumentsFilterProvider,
} from '../documents/context/DocumentsFilterContext';
+import { PreviewProvider } from '../preview/PreviewContext';
import { useWorkspaceSurface } from './useWorkspaceSurface';
import { DocumentsHeaderBreadcrumb } from '../documents/panel/DocumentsPanelHeader';
import { SidebarProvider, useSidebarContext } from '../sidebar/SidebarContext';
@@ -75,7 +76,9 @@ const DocumentsRouteContent: React.FC = () => {
const content = renderSurface();
return (
- {content}
+
+ {content}
+
);
};
diff --git a/frontend/src/desktop/DesktopWorkspace.tsx b/frontend/src/desktop/DesktopWorkspace.tsx
index 4aad89b..6665a8a 100644
--- a/frontend/src/desktop/DesktopWorkspace.tsx
+++ b/frontend/src/desktop/DesktopWorkspace.tsx
@@ -18,6 +18,7 @@ import { createDocumentEntryKey } from '../app/entryKey';
import { PointerTrackingProvider, usePointerTracking } from './PointerTrackingContext';
import type { Identifier } from '../types/identifiers';
import type { DocumentsListEntry, Document } from '../types/documents';
+import { usePreviewContext } from '../preview/PreviewContext';
type TagLike = { id?: Identifier | null; label?: string; color?: string | null } | null;
type OverlaySource = { url: string; alt?: string | null; mimeType?: string | null; };
@@ -50,7 +51,6 @@ export interface DesktopWorkspaceProps {
onDocumentTagDrop?: (docId: Identifier, tag: any) => void;
tenantId?: Identifier | null;
viewId?: string | null;
- onPreview?: (doc: DeskDocument) => void;
}
// Wrapper to handle hooks per card
@@ -84,8 +84,8 @@ const DesktopWorkspaceContent: React.FC = ({
onDocumentTagDrop,
tenantId,
viewId,
- onPreview,
}) => {
+ const { openPreview } = usePreviewContext();
const { addPointer, removePointer } = usePointerTracking();
const containerRef = useRef(null);
const [isLayoutReady, setIsLayoutReady] = useState(false);
@@ -207,16 +207,16 @@ const DesktopWorkspaceContent: React.FC = ({
// Preview the last selected document
const lastId = selectedDocumentIds[selectedDocumentIds.length - 1];
const doc = items.find(i => String(i.id) === lastId);
- if (doc && onPreview) {
+ if (doc) {
e.preventDefault();
- onPreview(doc);
+ openPreview(doc);
}
}
};
window.addEventListener('keydown', handleWindowKeyDown);
return () => window.removeEventListener('keydown', handleWindowKeyDown);
- }, [selectedDocumentIds, items, onPreview]);
+ }, [selectedDocumentIds, items, openPreview]);
return (
<>
diff --git a/frontend/src/documents/DocumentsView.tsx b/frontend/src/documents/DocumentsView.tsx
index f1e2732..bd86c19 100644
--- a/frontend/src/documents/DocumentsView.tsx
+++ b/frontend/src/documents/DocumentsView.tsx
@@ -2,6 +2,7 @@ import React, { useEffect, useCallback } from 'react';
import { useDocumentViewLogic, DocumentViewLogic } from './hooks/useDocumentViewLogic';
import { useDocumentsNavigation } from './hooks/useDocumentsNavigation';
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
+import { usePreviewContext } from '../preview/PreviewContext';
import DocumentsListRow from './components/DocumentsListRow';
import DocumentsGridCard from './components/DocumentsGridCard';
import DocumentsListContainer from './components/DocumentsListContainer';
@@ -21,7 +22,8 @@ const AbstractDocumentsView = void; chil
containerProps,
...props
}: AbstractDocumentsViewProps) => {
- const { entries, onDocumentRename, onFolderRename, onFolderSelect, onPreview, scrollRef, viewId } = props;
+ const { entries, onDocumentRename, onFolderRename, onFolderSelect, scrollRef, viewId } = props;
+ const { openPreview } = usePreviewContext();
const viewLogic = useDocumentViewLogic({
onDocumentRename,
onFolderRename,
@@ -29,7 +31,7 @@ const AbstractDocumentsView = void; chil
const { handleKeyDown, handleFocus } = useDocumentsNavigation({
entries,
onFolderSelect,
- onPreview,
+ onPreview: openPreview,
});
const { clearSelection } = viewLogic;
@@ -80,6 +82,7 @@ const AbstractDocumentsView = void; chil
entry={entry}
viewLogic={viewLogic}
{...props}
+ onPreview={openPreview}
/>
))}
diff --git a/frontend/src/documents/panel/DocumentsPanel.tsx b/frontend/src/documents/panel/DocumentsPanel.tsx
index b94967d..4156e45 100644
--- a/frontend/src/documents/panel/DocumentsPanel.tsx
+++ b/frontend/src/documents/panel/DocumentsPanel.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { DocumentsList, DocumentsGrid } from '../DocumentsView';
import type { DragEvent, ReactNode, RefObject } from 'react';
import type {
@@ -10,7 +10,7 @@ import type {
} from '../../types/documents';
import DesktopWorkspace from '../../desktop/DesktopWorkspace';
import { isTagTransferEvent } from '../tagTransfer';
-import PreviewZoomOverlay from '../../detail/PreviewZoomOverlay';
+import { usePreviewContext } from '../../preview/PreviewContext';
import { isPointerModifierEvent, isPrimaryPointerEvent } from '../useEntryPointer';
import {
WorkspaceSelectionProvider,
@@ -109,7 +109,6 @@ const DocumentsPanelInner: React.FC = ({
isSearchLoading = false,
viewMode = 'list',
onViewModeChange,
- ensureDownloadUrl,
onRefresh = () => { },
sortField,
sortDirection,
@@ -317,73 +316,7 @@ const DocumentsPanelInner: React.FC = ({
const isDeskView = viewMode === 'desk';
const gridIconSize = DEFAULT_GRID_ICON_SIZE;
- const [previewDoc, setPreviewDoc] = useState(null);
-
- useEffect(() => {
- if (previewDoc && !rows.find(d => d.id === previewDoc.id)) {
- setPreviewDoc(null);
- }
- }, [previewDoc, rows]);
-
- const [previewUrl, setPreviewUrl] = useState(null);
-
- const overlayDocument = useMemo(() => {
- if (!previewDoc || !previewUrl) {
- return previewDoc;
- }
- return {
- ...previewDoc,
- documentLink: {
- url: previewUrl,
- alt: previewDoc.title,
- mimeType: previewDoc.mime_type,
- },
- };
- }, [previewDoc, previewUrl]);
-
- useEffect(() => {
- if (!previewDoc) {
- setPreviewUrl(null);
- return;
- }
-
- let cancelled = false;
- if (ensureDownloadUrl) {
- ensureDownloadUrl(previewDoc.id)
- .then((entry) => {
- if (!cancelled && entry?.url) {
- setPreviewUrl(entry.url);
- }
- })
- .catch(() => {
- if (!cancelled) {
- setPreviewUrl(null);
- }
- });
- }
-
- return () => {
- cancelled = true;
- };
- }, [previewDoc, ensureDownloadUrl]);
-
- const closePreviewOverlay = useCallback(() => {
- setPreviewDoc(null);
- setPreviewUrl(null);
- }, []);
-
- const handleDocumentPreviewZoom = useCallback(
- (doc) => {
- if (!doc || !doc.id) {
- return;
- }
- if (!ensureDownloadUrl) {
- return;
- }
- setPreviewDoc(doc);
- },
- [ensureDownloadUrl],
- );
+ const { openPreview } = usePreviewContext();
const isTagDragEvent = useCallback((event) => isTagTransferEvent(event), []);
@@ -431,17 +364,15 @@ const DocumentsPanelInner: React.FC = ({
if (!doc) {
return;
}
- if (event) {
- event.preventDefault();
- event.stopPropagation();
- }
- if (event?.altKey) {
- handleDocumentPreviewZoom(doc);
+ if (event && (event.altKey || (event.button === 1))) {
+ openPreview(doc);
return;
}
- onDocumentActivate?.(doc.id);
+ if (onDocumentActivate) {
+ onDocumentActivate(doc);
+ }
},
- [handleDocumentPreviewZoom, onDocumentActivate],
+ [onDocumentActivate, openPreview],
);
const handleFolderClick = useCallback(
@@ -521,7 +452,6 @@ const DocumentsPanelInner: React.FC = ({
onCorrespondentClick: toggleCorrespondentFilter,
tenantId: currentTenantId,
viewId,
- onPreview: handleDocumentPreviewZoom,
};
const renderBody = () => {
@@ -578,11 +508,6 @@ const DocumentsPanelInner: React.FC = ({
>
{renderBody()}
-
>
);
};
diff --git a/frontend/src/preview/DocumentViewerPanel.tsx b/frontend/src/preview/DocumentViewerPanel.tsx
index 2610f0f..139dff7 100644
--- a/frontend/src/preview/DocumentViewerPanel.tsx
+++ b/frontend/src/preview/DocumentViewerPanel.tsx
@@ -1,9 +1,7 @@
import React, {
useCallback,
- useEffect,
useMemo,
useRef,
- useState,
} from 'react';
import type { ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
@@ -14,11 +12,11 @@ import {
IconX,
WindowMaximizeIcon,
} from '../ui/icons';
-import PreviewZoomOverlay from '../detail/PreviewZoomOverlay';
import {
buildCorrespondentOptions,
sortCorrespondents,
} from '../documents/DocumentSummarySection';
+import { usePreviewContext } from '../preview/PreviewContext';
import type { DocumentSummarySectionProps } from '../documents/DocumentSummarySection';
import { extractDocumentMetadataPayload } from '../documents/documentSummary';
import { createDocumentActionState } from '../documents/documentActions';
@@ -233,7 +231,17 @@ const DocumentViewerPanel: React.FC = ({
[hasOcr, loadOcrContent],
);
- const [zoomOverlayOpen, setZoomOverlayOpen] = useState(false);
+ const { openPreview } = usePreviewContext();
+
+ const handleZoomOpen = useCallback(() => {
+ if (!document) {
+ return;
+ }
+ openPreview(document);
+ }, [document, openPreview]);
+
+ const panelRef = useRef(null);
+ const isStackedLayout = useViewerLayoutMode(panelRef, document?.id);
const resolvedDocumentLink = useMemo(() => {
if (!document) {
@@ -253,24 +261,6 @@ const DocumentViewerPanel: React.FC = ({
};
}, [document]);
- const handleZoomOpen = useCallback(() => {
- if (!resolvedDocumentLink?.url) {
- return;
- }
- setZoomOverlayOpen(true);
- }, [resolvedDocumentLink?.url]);
-
- const handleZoomClose = useCallback(() => {
- setZoomOverlayOpen(false);
- }, []);
-
- useEffect(() => {
- setZoomOverlayOpen(false);
- }, [resolvedDocumentLink?.url, document?.id]);
-
- const panelRef = useRef(null);
- const isStackedLayout = useViewerLayoutMode(panelRef, document?.id);
-
const {
panelStyle: managedDetailPanelStyle,
handleProps: managedResizeHandleProps,
@@ -333,23 +323,11 @@ const DocumentViewerPanel: React.FC = ({
}));
}, [breadcrumbs, navigateToFolder]);
- const zoomDisplay = useMemo(() => {
- if (!resolvedDocumentLink?.url || !document) {
- return null;
- }
- const normalizedMimeType = document.mime_type;
- return {
- url: resolvedDocumentLink.url,
- alt: document.title,
- mimeType: normalizedMimeType,
- };
- }, [document, resolvedDocumentLink?.url]);
-
const headerActions = createDocumentViewerHeaderActions({
document,
actionState,
- onZoom: zoomDisplay ? handleZoomOpen : null,
- canZoom: Boolean(zoomDisplay),
+ onZoom: handleZoomOpen,
+ canZoom: Boolean(document),
});
const collapseButton = isSidebarVariant && onCollapsePanel
@@ -469,20 +447,6 @@ const DocumentViewerPanel: React.FC = ({
document?.title || 'Document preview'
);
- const overlayDocument = useMemo(() => (
- document && zoomDisplay?.url
- ? { ...document, documentLink: zoomDisplay }
- : document
- ), [document, zoomDisplay]);
-
- const overlay = (
-
- );
-
if (isSidebarVariant) {
const sidebarClass = `detail-panel panel${sidebarMode === 'inline' ? ' detail-panel--inline' : ''}${isPanelResizing ? ' detail-panel--resizing' : ''}`;
return (
@@ -501,7 +465,6 @@ const DocumentViewerPanel: React.FC = ({
/>
{viewerSection}
- {overlay}
>
);
}
@@ -517,7 +480,6 @@ const DocumentViewerPanel: React.FC = ({
/>
{viewerSection}
- {overlay}
>
);
};
diff --git a/frontend/src/preview/PreviewContext.tsx b/frontend/src/preview/PreviewContext.tsx
new file mode 100644
index 0000000..ba662e2
--- /dev/null
+++ b/frontend/src/preview/PreviewContext.tsx
@@ -0,0 +1,94 @@
+import React, { createContext, useContext, useState, useCallback, useMemo, useEffect } from 'react';
+import PreviewZoomOverlay from '../detail/PreviewZoomOverlay';
+import type { Document } from '../types/documents';
+import type { Identifier } from '../types/identifiers';
+
+interface PreviewContextType {
+ openPreview: (doc: Document) => void;
+ closePreview: () => void;
+}
+
+const PreviewContext = createContext(null);
+
+export const usePreviewContext = () => {
+ const context = useContext(PreviewContext);
+ if (!context) {
+ throw new Error('usePreviewContext must be used within a PreviewProvider');
+ }
+ return context;
+};
+
+interface PreviewProviderProps {
+ children: React.ReactNode;
+ ensureDownloadUrl?: (docId: Identifier) => Promise;
+}
+
+export const PreviewProvider: React.FC = ({ children, ensureDownloadUrl }) => {
+ const [previewDoc, setPreviewDoc] = useState(null);
+ const [previewUrl, setPreviewUrl] = useState(null);
+
+ const openPreview = useCallback((doc: Document) => {
+ setPreviewDoc(doc);
+ }, []);
+
+ const closePreview = useCallback(() => {
+ setPreviewDoc(null);
+ setPreviewUrl(null);
+ }, []);
+
+ useEffect(() => {
+ if (!previewDoc) {
+ setPreviewUrl(null);
+ return;
+ }
+
+ let cancelled = false;
+ if (ensureDownloadUrl) {
+ ensureDownloadUrl(previewDoc.id)
+ .then((entry) => {
+ if (!cancelled && entry?.url) {
+ setPreviewUrl(entry.url);
+ }
+ })
+ .catch(() => {
+ if (!cancelled) {
+ setPreviewUrl(null);
+ }
+ });
+ }
+
+ return () => {
+ cancelled = true;
+ };
+ }, [previewDoc, ensureDownloadUrl]);
+
+ const overlayDocument = useMemo(() => {
+ if (!previewDoc || !previewUrl) {
+ return previewDoc;
+ }
+ return {
+ ...previewDoc,
+ documentLink: {
+ url: previewUrl,
+ alt: previewDoc.title,
+ mimeType: previewDoc.mime_type,
+ },
+ };
+ }, [previewDoc, previewUrl]);
+
+ const value = useMemo(() => ({
+ openPreview,
+ closePreview,
+ }), [openPreview, closePreview]);
+
+ return (
+
+ {children}
+
+
+ );
+};