feat: Replaced document preview with a new fullscreen viewer

This commit is contained in:
2025-12-11 00:44:11 +01:00
parent f6bd6e2feb
commit e8636ce7da
15 changed files with 177 additions and 173 deletions
+5 -5
View File
@@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom';
import {
DocumentsFilterProvider,
} from '../documents/context/DocumentsFilterContext';
import { PreviewProvider, usePreviewContext } from '../viewer/PreviewContext';
import { FullscreenPreviewProvider, useFullscreenPreviewContext } from '../viewer/FullscreenPreviewContext';
import { DocumentOpenProvider } from '../lib/context/DocumentOpenContext';
import { useWorkspaceSurface } from './useWorkspaceSurface';
import { SidebarProvider, useSidebarContext } from '../sidebar/SidebarContext';
@@ -16,7 +16,7 @@ const DocumentsInner: React.FC<{
surfaceConfig: any;
onNavigate: (documentId: string) => void;
}> = ({ surfaceConfig, onNavigate }) => {
const { openPreview } = usePreviewContext();
const { openFullscreenPreview } = useFullscreenPreviewContext();
const { collapsed: sidebarCollapsed } = useSidebarContext();
const {
sidebarSuppressed,
@@ -68,7 +68,7 @@ const DocumentsInner: React.FC<{
return (
<DocumentOpenProvider
onOpenViewer={onNavigate}
onOpenPreview={openPreview}
onOpenFullscreenPreview={openFullscreenPreview}
onOpenDetailPanel={openDetailPanel}
>
{renderSurface()}
@@ -89,14 +89,14 @@ const DocumentsRouteContent: React.FC = () => {
return (
<DocumentsFilterProvider value={documentsFilter}>
<PreviewProvider
<FullscreenPreviewProvider
onNavigate={handleDocumentNavigate}
>
<DocumentsInner
surfaceConfig={surfaceConfig}
onNavigate={handleDocumentNavigate}
/>
</PreviewProvider>
</FullscreenPreviewProvider>
</DocumentsFilterProvider>
);
};
@@ -12,7 +12,7 @@ type FolderId = DocumentId | 'root';
import type { Document } from '../types/documents';
import useNotifyApiError from '../hooks/useNotifyApiError';
interface UseDocumentPreviewArgs {
interface UseDocumentViewerArgs {
routeDocumentId?: DocumentId | null;
documentsManager: {
getById: (id: DocumentId) => Document | null;
@@ -28,36 +28,36 @@ interface UseDocumentPreviewArgs {
open?: (args?: { documentIds?: DocumentId[] }) => void;
close?: () => void;
} | null>;
setActivePreviewId: Dispatch<SetStateAction<DocumentId | null>>;
setActiveViewerId: Dispatch<SetStateAction<DocumentId | null>>;
}
interface UseDocumentPreviewResult {
ensurePreviewData: (documentId: DocumentId) => Promise<Document | null>;
openDocumentPreview: (documentId: DocumentId, options?: { replace?: boolean }) => void;
closeDocumentPreview: (folderId?: FolderId) => void;
resetPreviewState: () => void;
previewWorkspaceDocument: Document | null;
previewActive: boolean;
interface UseDocumentViewerResult {
ensureViewerData: (documentId: DocumentId) => Promise<Document | null>;
openDocumentViewer: (documentId: DocumentId, options?: { replace?: boolean }) => void;
closeDocumentViewer: (folderId?: FolderId) => void;
resetViewerState: () => void;
viewerWorkspaceDocument: Document | null;
viewerActive: boolean;
}
const useDocumentPreview = ({
const useDocumentViewer = ({
routeDocumentId,
documentsManager,
selectedFolder,
locationPathname,
locationSearch,
detailPanelControlRef,
setActivePreviewId,
}: UseDocumentPreviewArgs): UseDocumentPreviewResult => {
const previewReturnPathRef = useRef<string | null>(null);
setActiveViewerId,
}: UseDocumentViewerArgs): UseDocumentViewerResult => {
const viewerReturnPathRef = useRef<string | null>(null);
const notifyApiError = useNotifyApiError();
const navigate = useNavigate();
const resetPreviewState = useCallback(() => {
previewReturnPathRef.current = null;
const resetViewerState = useCallback(() => {
viewerReturnPathRef.current = null;
}, []);
const ensurePreviewData = useCallback(
const ensureViewerData = useCallback(
async (documentId: DocumentId): Promise<Document | null> => {
if (!documentId) return null;
@@ -67,35 +67,35 @@ const useDocumentPreview = ({
throw new Error('Document metadata unavailable.');
}
if (!previewReturnPathRef.current) {
if (!viewerReturnPathRef.current) {
const fallbackFolderId = doc?.folder_id || 'root';
previewReturnPathRef.current =
viewerReturnPathRef.current =
fallbackFolderId === 'root' ? '/documents' : `/documents/folder/${fallbackFolderId}`;
}
setActivePreviewId(documentId);
setActiveViewerId(documentId);
return doc;
},
[
documentsManager,
setActivePreviewId,
setActiveViewerId,
],
);
const openDocumentPreview = useCallback(
const openDocumentViewer = useCallback(
(documentId: DocumentId, { replace = false }: { replace?: boolean } = {}) => {
if (!documentId) return;
detailPanelControlRef.current?.close?.();
previewReturnPathRef.current = `${locationPathname}${locationSearch}`;
viewerReturnPathRef.current = `${locationPathname}${locationSearch}`;
navigate(`/documents/${documentId}`, { replace });
},
[navigate, locationPathname, locationSearch, detailPanelControlRef],
);
const closeDocumentPreview = useCallback(
const closeDocumentViewer = useCallback(
(folderId?: FolderId) => {
const fallbackPath = previewReturnPathRef.current;
previewReturnPathRef.current = null;
const fallbackPath = viewerReturnPathRef.current;
viewerReturnPathRef.current = null;
if (fallbackPath) {
navigate(fallbackPath, { replace: false });
@@ -116,36 +116,36 @@ const useDocumentPreview = ({
let cancelled = false;
ensurePreviewData(routeDocumentId).catch((error) => {
ensureViewerData(routeDocumentId).catch((error) => {
if (cancelled) {
return;
}
notifyApiError(error, 'Failed to open document preview.');
closeDocumentPreview();
closeDocumentViewer();
});
return () => {
cancelled = true;
};
}, [routeDocumentId, ensurePreviewData, notifyApiError, closeDocumentPreview]);
}, [routeDocumentId, ensureViewerData, notifyApiError, closeDocumentViewer]);
const previewWorkspaceDocument = useMemo(() => {
const viewerWorkspaceDocument = useMemo(() => {
if (!routeDocumentId) {
return null;
}
return documentsManager.getById(routeDocumentId);
}, [routeDocumentId, documentsManager]);
const previewActive = Boolean(routeDocumentId && previewWorkspaceDocument);
const viewerActive = Boolean(routeDocumentId && viewerWorkspaceDocument);
return {
ensurePreviewData,
openDocumentPreview,
closeDocumentPreview,
resetPreviewState,
previewWorkspaceDocument,
previewActive,
ensureViewerData,
openDocumentViewer,
closeDocumentViewer,
resetViewerState,
viewerWorkspaceDocument,
viewerActive,
};
};
export default useDocumentPreview;
export default useDocumentViewer;
+4 -4
View File
@@ -30,10 +30,10 @@ const useDocumentsShell = (): DocumentsShellView => {
detailPanelOpen: Boolean(shell.detailPanel?.detailPanelOpen),
openDetailPanel: shell.detailPanel?.openDetailPanel as WorkspaceSurfaceConfig['openDetailPanel'],
closeDetailPanel: shell.detailPanel?.closeDetailPanel as WorkspaceSurfaceConfig['closeDetailPanel'],
previewWorkspaceDocument: shell.preview?.previewWorkspaceDocument,
previewDocumentId: (shell.preview?.previewDocumentId as Identifier) ?? null,
closeDocumentPreview: shell.preview?.closeDocumentPreview as WorkspaceSurfaceConfig['closeDocumentPreview'],
ensurePreviewData: shell.preview?.ensurePreviewData as WorkspaceSurfaceConfig['ensurePreviewData'],
viewerWorkspaceDocument: shell.preview?.viewerWorkspaceDocument,
viewerDocumentId: (shell.preview?.viewerDocumentId as Identifier) ?? null,
closeDocumentViewer: shell.preview?.closeDocumentViewer as WorkspaceSurfaceConfig['closeDocumentViewer'],
ensureViewerData: shell.preview?.ensureViewerData as WorkspaceSurfaceConfig['ensureViewerData'],
ensureAssetUrl: shell.preview?.ensureAssetUrl as WorkspaceSurfaceConfig['ensureAssetUrl'],
getDocumentAsset: shell.preview?.getDocumentAsset as WorkspaceSurfaceConfig['getDocumentAsset'],
notifyApiError: shell.ui?.notifyApiError as WorkspaceSurfaceConfig['notifyApiError'],
+26 -26
View File
@@ -12,14 +12,14 @@ type EnsureAssetUrl = (
asset: unknown,
options?: Record<string, unknown>,
) => Promise<unknown> | void;
type EnsurePreviewData = (docId: Identifier, options?: Record<string, unknown>) => Promise<unknown>;
type EnsureViewerData = (docId: Identifier, options?: Record<string, unknown>) => Promise<unknown>;
type GetDocumentAsset = (document: unknown, assetType: string) => unknown;
type NotifyApiError = (error: unknown, fallbackMessage?: string) => void;
type DocumentsTableProps = ComponentProps<typeof DocumentsPanel> | null;
type DetailPanelProps = (ComponentProps<typeof DocumentViewerPanel> & {
onClose?: () => void;
onOpenPreview?: (args: { documentIds: Array<string> }) => void;
onOpenViewer?: (args: { documentIds: Array<string> }) => void;
folderNodes?: Map<Identifier | 'root', unknown>;
ensureFolderData?: (
folderId: Identifier | 'root',
@@ -35,13 +35,13 @@ export interface UseWorkspaceSurfaceArgs {
documentsTableProps?: DocumentsTableProps;
detailPanelProps?: DetailPanelProps;
detailPanelOpen?: boolean;
previewWorkspaceDocument?: unknown;
previewDocumentId?: Identifier | null;
viewerWorkspaceDocument?: unknown;
viewerDocumentId?: Identifier | null;
ensureAssetUrl?: EnsureAssetUrl;
ensurePreviewData?: EnsurePreviewData;
ensureViewerData?: EnsureViewerData;
getDocumentAsset?: GetDocumentAsset;
notifyApiError?: NotifyApiError;
closeDocumentPreview?: () => void;
closeDocumentViewer?: () => void;
}
interface UseWorkspaceSurfaceResult {
@@ -54,13 +54,13 @@ export const useWorkspaceSurface = ({
documentsTableProps,
detailPanelProps,
detailPanelOpen = false,
previewWorkspaceDocument,
previewDocumentId,
viewerWorkspaceDocument,
viewerDocumentId,
ensureAssetUrl,
ensurePreviewData,
ensureViewerData,
getDocumentAsset,
notifyApiError,
closeDocumentPreview,
closeDocumentViewer,
}: UseWorkspaceSurfaceArgs): UseWorkspaceSurfaceResult => {
const { registerDetailCloseHandler, setDetailActive } = usePanelManager();
@@ -118,7 +118,7 @@ export const useWorkspaceSurface = ({
? (() => {
const {
onClose,
onOpenPreview,
onOpenViewer,
tags: tagOptions,
folderNodes,
ensureFolderData,
@@ -129,7 +129,7 @@ export const useWorkspaceSurface = ({
variant="sidebar"
sidebarMode={sidebarMode}
onClose={onClose}
onMaximize={onOpenPreview}
onMaximize={onOpenViewer}
tagOptions={tagOptions}
{...restDetailProps}
/>
@@ -164,10 +164,10 @@ export const useWorkspaceSurface = ({
detailPanelProps,
]);
const showPreviewWorkspace = Boolean(previewDocumentId);
const showViewerWorkspace = Boolean(viewerDocumentId);
const previewSurface = useMemo<WorkspaceSurface>(() => {
if (!showPreviewWorkspace) {
const viewerSurface = useMemo<WorkspaceSurface>(() => {
if (!showViewerWorkspace) {
return null;
}
@@ -191,8 +191,8 @@ export const useWorkspaceSurface = ({
const viewer = (
<DocumentViewerPanel
document={previewWorkspaceDocument || null}
hydrateDocument={ensurePreviewData}
document={viewerWorkspaceDocument || null}
hydrateDocument={ensureViewerData}
tagLookupById={tagLookupById}
tagOptions={tagOptions}
onTagAdd={onTagAdd}
@@ -205,10 +205,10 @@ export const useWorkspaceSurface = ({
onUpdateIssued={onUpdateIssued}
ensureAssetUrl={ensureAssetUrl}
getDocumentAsset={getDocumentAsset}
ensurePreviewData={ensurePreviewData}
ensurePreviewData={ensureViewerData}
notifyApiError={notifyApiError}
sidebarToggle={sidebarToggle}
onClose={closeDocumentPreview}
onClose={closeDocumentViewer}
resolveFolderPath={resolveFolderPath}
/>
);
@@ -223,23 +223,23 @@ export const useWorkspaceSurface = ({
return { content, detail: null, detailMode: null };
}, [
showPreviewWorkspace,
previewWorkspaceDocument,
ensurePreviewData,
showViewerWorkspace,
viewerWorkspaceDocument,
ensureViewerData,
ensureAssetUrl,
getDocumentAsset,
notifyApiError,
renderSidebarToggle,
closeDocumentPreview,
closeDocumentViewer,
detailPanelProps,
]);
const surface = useMemo<WorkspaceSurface>(() => {
if (showPreviewWorkspace) {
return previewSurface;
if (showViewerWorkspace) {
return viewerSurface;
}
return documentsSurface;
}, [showPreviewWorkspace, previewSurface, documentsSurface]);
}, [showViewerWorkspace, viewerSurface, documentsSurface]);
return { surface };
};
@@ -57,8 +57,8 @@ interface UseDocumentMutationsArgs {
selectionState: SelectionState;
tagsState: TagsState;
correspondentsState: CorrespondentsState;
closeDocumentPreview: () => void;
previewDocumentId?: DocumentId | null;
closeDocumentViewer: () => void;
viewerDocumentId?: DocumentId | null;
resolveTargetDocumentIds: (ids?: Identifier[] | null) => Identifier[];
}
@@ -103,8 +103,8 @@ const useDocumentMutations = ({
selectionState,
tagsState,
correspondentsState,
closeDocumentPreview,
previewDocumentId,
closeDocumentViewer,
viewerDocumentId,
resolveTargetDocumentIds,
}: UseDocumentMutationsArgs): UseDocumentMutationsResult => {
const { showToast } = useStatusToast();
@@ -140,14 +140,14 @@ const useDocumentMutations = ({
await queueDocumentReanalysis(documentId);
showToast('Analysis queued.', 'info');
// Close preview if it's the current one to allow refresh?
if (previewDocumentId === documentId) {
closeDocumentPreview();
if (viewerDocumentId === documentId) {
closeDocumentViewer();
}
} catch (error) {
notifyApiError(error, 'Failed to queue analysis.');
}
},
[closeDocumentPreview, notifyApiError, previewDocumentId, showToast],
[closeDocumentViewer, notifyApiError, viewerDocumentId, showToast],
);
const handleDocumentsDelete = useCallback(
@@ -55,7 +55,7 @@ import { useAppDispatch, useAppState } from '../../lib/store/appState';
import { listFolderContents } from '../../lib/api/apiClient';
import { useApi } from '../../lib/context/ApiContext';
import { useWorkspaceSelection } from '../../app/useWorkspaceSelection';
import useDocumentPreview from '../../app/useDocumentPreview';
import useDocumentViewer from '../../app/useDocumentViewer';
import type { DocumentId, FolderNodeId, Identifier } from '../../types/identifiers';
import type { Document } from '../../types/documents';
@@ -117,7 +117,7 @@ const useDocumentsWorkspace = ({
const docMatch = matchPath('/documents/:documentId', location.pathname);
const routeFolderId = folderMatch?.params?.folderId || null;
const routeDocumentId = docMatch?.params?.documentId || null;
const previewDocumentId = routeDocumentId;
const viewerDocumentId = routeDocumentId;
const handleBreadcrumbNavigate = useCallback((crumb: { id?: Identifier | string } | null) => {
if (!crumb || !crumb.id) {
@@ -157,7 +157,7 @@ const useDocumentsWorkspace = ({
const [draggedDocumentIds, setDraggedDocumentIds] = useState<DocumentId[]>([]);
const [draggedFolderId, setDraggedFolderId] = useState<FolderNodeId | null>(null);
const [activePreviewId, setActivePreviewId] = useState<DocumentId | null>(routeDocumentId || null);
const [activeViewerId, setActiveViewerId] = useState<DocumentId | null>(routeDocumentId || null);
const shellRef = useRef(null);
const assetManagerRef = useRef(null);
if (!assetManagerRef.current) {
@@ -402,7 +402,7 @@ const useDocumentsWorkspace = ({
documentsManager,
});
const documentsFilter = documentsSearch.documentsFilterValue;
const showingSearchResults = documentsSearch.searchResultIds !== null;
// Live Filter: Ensure we only show documents that actually belong to the current folder.
@@ -432,30 +432,30 @@ const useDocumentsWorkspace = ({
});
const {
openDocumentPreview,
closeDocumentPreview,
resetPreviewState,
previewWorkspaceDocument,
previewActive,
} = useDocumentPreview({
routeDocumentId: previewDocumentId,
openDocumentViewer,
closeDocumentViewer,
resetViewerState,
viewerWorkspaceDocument,
viewerActive,
} = useDocumentViewer({
routeDocumentId: viewerDocumentId,
documentsManager,
selectedFolder,
locationPathname: location.pathname,
locationSearch: location.search,
detailPanelControlRef,
setActivePreviewId,
setActiveViewerId,
});
const openDocumentPreviewForDetail = useCallback(
const openDocumentViewerForDetail = useCallback(
({ documentIds }: { documentIds?: Identifier[] } = {}) => {
const targetId = documentIds?.find((value): value is Identifier => value != null);
if (targetId == null) {
return;
}
openDocumentPreview(targetId, { replace: true });
openDocumentViewer(targetId, { replace: true });
},
[openDocumentPreview],
[openDocumentViewer],
);
const getDocumentAsset = useCallback((doc, type) => {
@@ -475,8 +475,8 @@ const useDocumentsWorkspace = ({
selectionAnchorRef,
setFocusedDocumentId,
selectedDocumentIds,
activePreviewId,
setActivePreviewId,
activeViewerId,
setActiveViewerId,
selectionInitializedRef,
});
@@ -573,10 +573,10 @@ const useDocumentsWorkspace = ({
documentsSearch.setSearchQuery('');
documentsSearch.setActiveTagFilters([]);
documentsSearch.setActiveCorrespondentFilters([]);
setActivePreviewId(null);
setActiveViewerId(null);
detailPanelControlRef.current.close();
assetManager.reset();
resetPreviewState();
resetViewerState();
upload.resetUploadsState();
upload.clearUploadQueue();
@@ -598,8 +598,8 @@ const useDocumentsWorkspace = ({
setDraggedDocumentIds,
setDraggedFolderId,
documentsSearch,
setActivePreviewId,
resetPreviewState,
setActiveViewerId,
resetViewerState,
upload,
]);
@@ -631,8 +631,8 @@ const useDocumentsWorkspace = ({
...correspondentsState,
correspondentManager,
},
closeDocumentPreview,
previewDocumentId,
closeDocumentViewer,
viewerDocumentId,
resolveTargetDocumentIds,
});
@@ -693,7 +693,7 @@ const useDocumentsWorkspace = ({
selectionAnchorRef,
promoteSelectionOrderRaw,
setFocusedDocumentId,
setActivePreviewId,
setActiveViewerId,
clearSelection,
focusedDocumentId,
setFocusedEntryKey,
@@ -884,12 +884,12 @@ const useDocumentsWorkspace = ({
return () => window.removeEventListener('keydown', handleKeyDown);
}, [settingsOpen]);
const detailPanel = useDetailWorkspace({
const detailWorkspace = useDetailWorkspace({
documentLookup,
folderNodes,
detailPanelControlRef,
detailFolderFetchRef,
openDocumentPreview: openDocumentPreviewForDetail,
openDocumentViewer: openDocumentViewerForDetail,
handleDocumentTitleUpdate,
handleDocumentIssuedUpdate,
handleDocumentTagAdd,
@@ -906,6 +906,8 @@ const useDocumentsWorkspace = ({
resolveFolderPath,
});
const handleEntryPointerCore = useEntryPointerCore({
onSelectEntry: (entry, event, { rowKey, modifierClick, primaryClick }) => {
const { type, id } = entry;
@@ -979,17 +981,18 @@ const useDocumentsWorkspace = ({
};
const preview = {
previewActive,
previewWorkspaceDocument,
previewDocumentId,
closeDocumentPreview,
openDocumentViewerForDetail,
viewerActive,
viewerWorkspaceDocument,
viewerDocumentId,
closeDocumentViewer,
selectionContext,
ensureAssetUrl,
getDocumentAsset,
};
const search = {
searchQuery: documentsSearch.searchQuery,
documentsFilter,
searchLoading: documentsSearch.searchLoading,
documentsViewMode,
handleDocumentsViewModeChange,
@@ -999,6 +1002,7 @@ const useDocumentsWorkspace = ({
handleDocumentsSortDirectionToggle,
searchResultIds: documentsSearch.searchResultIds,
documents: viewDocuments,
documentsFilter: documentsSearch.documentsFilterValue,
};
const folderTree = {
@@ -1043,7 +1047,7 @@ const useDocumentsWorkspace = ({
correspondents,
passkeys,
preview,
detailPanel,
detailPanel: detailWorkspace,
search,
folderTree,
selection,
@@ -29,7 +29,7 @@ interface UseDocumentsSelectionOptions {
selectionAnchorRef: { current: string | null };
promoteSelectionOrderRaw: (id: DocumentId) => void;
setFocusedDocumentId: (id: DocumentId | null) => void;
setActivePreviewId: (id: DocumentId | null) => void;
setActiveViewerId: (id: DocumentId | null) => void;
clearSelection: () => void;
focusedDocumentId: DocumentId | null;
setFocusedEntryKey: Dispatch<SetStateAction<string | null>>;
@@ -46,7 +46,7 @@ const useDocumentsSelection = ({
selectionAnchorRef,
promoteSelectionOrderRaw,
setFocusedDocumentId,
setActivePreviewId,
setActiveViewerId,
clearSelection,
focusedDocumentId,
setFocusedEntryKey,
@@ -92,9 +92,9 @@ const useDocumentsSelection = ({
selectionAnchorRef.current = rowKey;
}
setFocusedDocumentId(docId);
setActivePreviewId(docId);
setActiveViewerId(docId);
},
[promoteSelectionOrderRaw, selectionAnchorRef, setFocusedDocumentId, setActivePreviewId],
[promoteSelectionOrderRaw, selectionAnchorRef, setFocusedDocumentId, setActiveViewerId],
);
const clearDocumentSelection = useCallback(() => {
@@ -11,8 +11,8 @@ interface UseWorkspaceSelectionSyncArgs {
selectionAnchorRef: MutableRefObject<Identifier | string | null>;
setFocusedDocumentId: (id: Identifier | null) => void;
selectedDocumentIds: Identifier[];
activePreviewId: Identifier | null;
setActivePreviewId: (id: Identifier | null) => void;
activeViewerId: Identifier | null;
setActiveViewerId: (id: Identifier | null) => void;
selectionInitializedRef: MutableRefObject<boolean>;
}
@@ -25,8 +25,8 @@ const useWorkspaceSelectionSync = ({
selectionAnchorRef,
setFocusedDocumentId,
selectedDocumentIds,
activePreviewId,
setActivePreviewId,
activeViewerId,
setActiveViewerId,
selectionInitializedRef,
}: UseWorkspaceSelectionSyncArgs) => {
useEffect(() => {
@@ -52,11 +52,11 @@ const useWorkspaceSelectionSync = ({
if (!selectedDocumentIds.length) {
return;
}
if (!selectedDocumentIds.includes(activePreviewId as Identifier)) {
setActivePreviewId(selectedDocumentIds[selectedDocumentIds.length - 1]);
if (!selectedDocumentIds.includes(activeViewerId as Identifier)) {
setActiveViewerId(selectedDocumentIds[selectedDocumentIds.length - 1]);
}
selectionInitializedRef.current = true;
}, [selectedDocumentIds, activePreviewId, selectionInitializedRef, setActivePreviewId]);
}, [selectedDocumentIds, activeViewerId, selectionInitializedRef, setActiveViewerId]);
};
export default useWorkspaceSelectionSync;
@@ -14,14 +14,14 @@ const [DocumentOpenContext, useDocumentOpen] = createSafeContext<DocumentOpenCon
interface DocumentOpenProviderProps {
children: React.ReactNode;
onOpenViewer?: (docId: Identifier) => void;
onOpenPreview?: (doc: Document) => void;
onOpenFullscreenPreview?: (doc: Document) => void;
onOpenDetailPanel?: (docId: Identifier) => void;
}
export const DocumentOpenProvider: React.FC<DocumentOpenProviderProps> = ({
children,
onOpenViewer,
onOpenPreview,
onOpenFullscreenPreview,
onOpenDetailPanel,
}) => {
const openDocument = useCallback((doc: Document, intent: DocumentOpenIntent = 'preview') => {
@@ -29,8 +29,8 @@ export const DocumentOpenProvider: React.FC<DocumentOpenProviderProps> = ({
switch (intent) {
case 'preview':
if (onOpenPreview) {
onOpenPreview(doc);
if (onOpenFullscreenPreview) {
onOpenFullscreenPreview(doc);
}
break;
case 'inspect':
@@ -51,7 +51,7 @@ export const DocumentOpenProvider: React.FC<DocumentOpenProviderProps> = ({
}
break;
}
}, [onOpenPreview, onOpenDetailPanel, onOpenViewer]);
}, [onOpenFullscreenPreview, onOpenDetailPanel, onOpenViewer]);
return (
<DocumentOpenContext.Provider value={{ openDocument }}>
+2 -2
View File
@@ -2,7 +2,7 @@
@import './base/controls.css';
@import './layout/panel-header-controls.css';
@import './components/resize-handle.css';
@import '../viewer/preview-zoom.css';
@import '../viewer/fullscreen-preview.css';
@import './base/iconography.css';
@import '../documents/styles/controls.css';
@import './layout/structure.css';
@@ -21,4 +21,4 @@
@import '../login/login.css';
@import '../documents/styles/shared-snippets.css';
@import './uploads/overlay.css';
@import './base/text-button.css';
@import './base/text-button.css';
+4 -4
View File
@@ -16,7 +16,7 @@ import {
sortCorrespondents,
} from './components/DocumentSummarySection';
import DocumentDownloadLink from '../documents/components/DocumentDownloadLink';
import { usePreviewContext } from './PreviewContext';
import { useFullscreenPreviewContext } from './FullscreenPreviewContext';
import type { DocumentSummarySectionProps } from './components/DocumentSummarySection';
import { extractDocumentMetadataPayload } from './logic/documentSummary';
import { resolveDocumentAssetUrl } from '../lib/assets/AssetManager';
@@ -218,14 +218,14 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
[hasOcr, loadOcrContent],
);
const { openPreview } = usePreviewContext();
const { openFullscreenPreview } = useFullscreenPreviewContext();
const handleZoomOpen = useCallback(() => {
if (!document) {
return;
}
openPreview(document);
}, [document, openPreview]);
openFullscreenPreview(document);
}, [document, openFullscreenPreview]);
const panelRef = useRef<HTMLDivElement | HTMLFormElement | HTMLElement | null>(null);
const isStackedLayout = useViewerLayoutMode(panelRef, document?.id);
@@ -1,33 +1,33 @@
import React, { useState, useCallback, useMemo, useRef } from 'react';
import PreviewZoomOverlay from './components/PreviewZoomOverlay';
import FullscreenPreviewOverlay from './components/FullscreenPreviewOverlay';
import type { Document } from '../types/documents';
import type { Identifier } from '../types/identifiers';
import { createSafeContext } from '../utils/createSafeContext';
interface PreviewContextType {
openPreview: (doc: Document) => void;
closePreview: () => void;
interface FullscreenPreviewContextType {
openFullscreenPreview: (doc: Document) => void;
closeFullscreenPreview: () => void;
}
const [PreviewContext, usePreviewContext] = createSafeContext<PreviewContextType>('Preview');
const [FullscreenPreviewContext, useFullscreenPreviewContext] = createSafeContext<FullscreenPreviewContextType>('FullscreenPreview');
interface PreviewProviderProps {
interface FullscreenPreviewProviderProps {
children: React.ReactNode;
onNavigate?: (documentId: Identifier) => void;
}
export const PreviewProvider: React.FC<PreviewProviderProps> = ({ children, onNavigate }) => {
export const FullscreenPreviewProvider: React.FC<FullscreenPreviewProviderProps> = ({ children, onNavigate }) => {
const [previewDoc, setPreviewDoc] = useState<Document | null>(null);
const lastFocusedElement = useRef<HTMLElement | null>(null);
const openPreview = useCallback((doc: Document) => {
const openFullscreenPreview = useCallback((doc: Document) => {
if (!lastFocusedElement.current) {
lastFocusedElement.current = document.activeElement as HTMLElement;
}
setPreviewDoc(doc);
}, []);
const closePreview = useCallback(() => {
const closeFullscreenPreview = useCallback(() => {
setPreviewDoc(null);
if (lastFocusedElement.current) {
lastFocusedElement.current.focus();
@@ -38,26 +38,26 @@ export const PreviewProvider: React.FC<PreviewProviderProps> = ({ children, onNa
const handleMaximize = useCallback(() => {
if (previewDoc && onNavigate) {
onNavigate(previewDoc.id);
closePreview();
closeFullscreenPreview();
}
}, [previewDoc, onNavigate, closePreview]);
}, [previewDoc, onNavigate, closeFullscreenPreview]);
const value = useMemo(() => ({
openPreview,
closePreview,
}), [openPreview, closePreview]);
openFullscreenPreview,
closeFullscreenPreview,
}), [openFullscreenPreview, closeFullscreenPreview]);
return (
<PreviewContext.Provider value={value}>
<FullscreenPreviewContext.Provider value={value}>
{children}
<PreviewZoomOverlay
<FullscreenPreviewOverlay
open={Boolean(previewDoc)}
onClose={closePreview}
onClose={closeFullscreenPreview}
onMaximize={onNavigate ? handleMaximize : undefined}
document={previewDoc}
/>
</PreviewContext.Provider>
</FullscreenPreviewContext.Provider>
);
};
export { usePreviewContext };
export { useFullscreenPreviewContext };
@@ -7,14 +7,14 @@ import { IconX, FileInfoIcon } from '../../components/icons';
import type { Document } from '../../types/documents';
interface PreviewZoomOverlayProps {
interface FullscreenPreviewOverlayProps {
open?: boolean;
onClose: () => void;
onMaximize?: () => void;
document?: Document | null;
}
const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
const FullscreenPreviewOverlay: React.FC<FullscreenPreviewOverlayProps> = ({
open = false,
onClose,
onMaximize,
@@ -81,12 +81,12 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
}
};
const stageClassName = 'preview-zoom__stage';
const containerClassName = 'preview-zoom__scroll';
const stageClassName = 'fullscreen-preview__stage';
const containerClassName = 'fullscreen-preview__scroll';
const backdropClassName = [
'preview-zoom-backdrop',
isBackdropVisible ? 'preview-zoom-backdrop--visible' : '',
'fullscreen-preview-backdrop',
isBackdropVisible ? 'fullscreen-preview-backdrop--visible' : '',
]
.filter(Boolean)
.join(' ');
@@ -160,4 +160,4 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
);
};
export default PreviewZoomOverlay;
export default FullscreenPreviewOverlay;
@@ -1,4 +1,4 @@
.preview-zoom-backdrop {
.fullscreen-preview-backdrop {
position: fixed;
inset: 0;
background: var(--overlay-backdrop);
@@ -14,13 +14,13 @@
-webkit-backdrop-filter: blur(8px);
}
.preview-zoom-backdrop--visible {
.fullscreen-preview-backdrop--visible {
opacity: 1;
background: var(--overlay-backdrop);
pointer-events: auto;
}
.preview-zoom__stage {
.fullscreen-preview__stage {
position: relative;
display: flex;
align-items: center;
@@ -32,7 +32,7 @@
--header-clearance: calc(2.5rem + 0.5rem);
}
.preview-zoom__scroll .document-viewer__object--image {
.fullscreen-preview__scroll .document-viewer__object--image {
width: auto;
height: auto;
box-shadow: 0 32px 120px var(--shadow-deep);
@@ -42,7 +42,7 @@
touch-action: manipulation;
}
.preview-zoom__scroll {
.fullscreen-preview__scroll {
display: flex;
align-items: center;
justify-content: center;
@@ -55,21 +55,21 @@
--pdf-viewer-stack-padding: var(--preview-padding);
}
.preview-zoom__scroll:focus {
.fullscreen-preview__scroll:focus {
outline: none;
}
.preview-zoom__scroll .document-viewer__object--pdf {
.fullscreen-preview__scroll .document-viewer__object--pdf {
width: 100%;
height: 100%;
}
.preview-zoom__stage .pdf-viewer__page-wrapper {
.fullscreen-preview__stage .pdf-viewer__page-wrapper {
position: relative;
overflow: visible;
}
.preview-zoom__stage .pdf-viewer__page-wrapper::after {
.fullscreen-preview__stage .pdf-viewer__page-wrapper::after {
content: '';
position: absolute;
inset: 0;
@@ -80,7 +80,7 @@
}
/* Header positioning for preview zoom overlay */
.preview-zoom-backdrop .panel-header--dark {
.fullscreen-preview-backdrop .panel-header--dark {
position: absolute;
top: 0;
left: 0;
@@ -94,6 +94,6 @@
}
/* Prevent header clicks from closing backdrop */
.preview-zoom-backdrop .panel-header--dark * {
.fullscreen-preview-backdrop .panel-header--dark * {
pointer-events: auto;
}
@@ -22,7 +22,7 @@ interface UseDetailWorkspaceArgs {
folderNodes: Map<Identifier | 'root', FolderNode>;
detailPanelControlRef: MutableRefObject<{ open?: (documentId: Identifier) => void; close?: () => void } | null>;
detailFolderFetchRef: MutableRefObject<Set<Identifier | 'root'>>;
openDocumentPreview?: (args: { documentIds: Identifier[] }) => void;
openDocumentViewer?: (args: { documentIds: Identifier[] }) => void;
handleDocumentTitleUpdate?: (docId: Identifier, title: string) => Promise<boolean> | boolean;
handleDocumentIssuedUpdate?: (docId: Identifier, issued: number | null) => Promise<boolean> | boolean;
handleDocumentTagAdd?: (doc: Document, value: string, context?: { option?: unknown }) => void;
@@ -54,7 +54,7 @@ const useDetailWorkspace = ({
folderNodes,
detailPanelControlRef,
detailFolderFetchRef,
openDocumentPreview,
openDocumentViewer,
handleDocumentTitleUpdate,
handleDocumentIssuedUpdate,
handleDocumentTagAdd,
@@ -176,7 +176,7 @@ const useDetailWorkspace = ({
correspondentLookupById,
onTagAdd: handleDetailTagAdd,
onTagRemove: handleDetailTagRemove,
onOpenPreview: openDocumentPreview,
onOpenViewer: openDocumentViewer,
onUpdateTitle: handleDocumentTitleUpdate,
onUpdateIssued: handleDocumentIssuedUpdate,
ensureAssetUrl,