feat: Add OCR script for PDFs using Ollama and refine document preview navigation and zoom behavior.
This commit is contained in:
@@ -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 (
|
||||
<DocumentsFilterProvider value={documentsFilter}>
|
||||
<PreviewProvider ensureDownloadUrl={surfaceConfig.documentsTableProps?.ensureDownloadUrl}>
|
||||
<PreviewProvider
|
||||
ensureDownloadUrl={surfaceConfig.documentsTableProps?.ensureDownloadUrl}
|
||||
onNavigate={handleDocumentNavigate}
|
||||
>
|
||||
{content}
|
||||
</PreviewProvider>
|
||||
</DocumentsFilterProvider>
|
||||
|
||||
@@ -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<PreviewZoomOverlayProps> = ({
|
||||
open = false,
|
||||
onClose = noop,
|
||||
onMaximize,
|
||||
document: overlayDocument = null,
|
||||
}) => {
|
||||
const portalTarget = document.body;
|
||||
@@ -235,25 +239,10 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
||||
|
||||
};
|
||||
|
||||
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<HTMLElement>) => {
|
||||
@@ -287,7 +276,7 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
||||
|
||||
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<PreviewZoomOverlayProps> = ({
|
||||
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<PreviewZoomOverlayProps> = ({
|
||||
|
||||
const effectiveDisplay = documentLink;
|
||||
|
||||
const downloadUrl = activeDocument?.current_version?.download?.url;
|
||||
|
||||
return createPortal(
|
||||
(
|
||||
<div
|
||||
@@ -317,6 +308,47 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
||||
onClick={onClose}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<PanelHeader
|
||||
className="panel-header--dark"
|
||||
title={documentTitle}
|
||||
leading={
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="icon-button"
|
||||
aria-label="Close preview"
|
||||
type="button"
|
||||
>
|
||||
<IconX />
|
||||
</button>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
{downloadUrl && (
|
||||
<a
|
||||
href={downloadUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="icon-button"
|
||||
aria-label="Download document"
|
||||
title="Download document"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</a>
|
||||
)}
|
||||
{onMaximize && (
|
||||
<button
|
||||
onClick={onMaximize}
|
||||
className="icon-button"
|
||||
aria-label="Open document info"
|
||||
title="Open document info"
|
||||
type="button"
|
||||
>
|
||||
<FileInfoIcon />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className={stageClassName}
|
||||
onClick={(event) => {
|
||||
|
||||
@@ -379,7 +379,8 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
|
||||
const stackStyle = useMemo<CSSProperties>(() => ({
|
||||
'--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'));
|
||||
|
||||
@@ -21,9 +21,10 @@ export const usePreviewContext = () => {
|
||||
interface PreviewProviderProps {
|
||||
children: React.ReactNode;
|
||||
ensureDownloadUrl?: (docId: Identifier) => Promise<any>;
|
||||
onNavigate?: (documentId: Identifier) => void;
|
||||
}
|
||||
|
||||
export const PreviewProvider: React.FC<PreviewProviderProps> = ({ children, ensureDownloadUrl }) => {
|
||||
export const PreviewProvider: React.FC<PreviewProviderProps> = ({ children, ensureDownloadUrl, onNavigate }) => {
|
||||
const [previewDoc, setPreviewDoc] = useState<Document | null>(null);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
|
||||
@@ -36,6 +37,13 @@ export const PreviewProvider: React.FC<PreviewProviderProps> = ({ 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<PreviewProviderProps> = ({ children, ensu
|
||||
<PreviewZoomOverlay
|
||||
open={Boolean(previewUrl)}
|
||||
onClose={closePreview}
|
||||
onMaximize={onNavigate ? handleMaximize : undefined}
|
||||
document={overlayDocument}
|
||||
/>
|
||||
</PreviewContext.Provider>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user