feat: Add Download type and document download field, unify preview zoom styling
This commit is contained in:
@@ -81,7 +81,6 @@ const DocumentsRouteContent: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<DocumentsFilterProvider value={documentsFilter}>
|
<DocumentsFilterProvider value={documentsFilter}>
|
||||||
<PreviewProvider
|
<PreviewProvider
|
||||||
ensureDownloadUrl={surfaceConfig.documentsTableProps?.ensureDownloadUrl}
|
|
||||||
onNavigate={handleDocumentNavigate}
|
onNavigate={handleDocumentNavigate}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { CSSProperties, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { clamp } from '../utils/math';
|
|
||||||
import PdfViewer from '../preview/PdfViewer';
|
import PdfViewer from '../preview/PdfViewer';
|
||||||
import MediaViewer from '../preview/MediaViewer';
|
import MediaViewer from '../preview/MediaViewer';
|
||||||
import PanelHeader from '../ui/PanelHeader';
|
import PanelHeader from '../ui/PanelHeader';
|
||||||
@@ -10,199 +9,72 @@ import type { Document } from '../types/documents';
|
|||||||
|
|
||||||
interface PreviewZoomOverlayProps {
|
interface PreviewZoomOverlayProps {
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
onClose?: () => void;
|
onClose: () => void;
|
||||||
onMaximize?: () => void;
|
onMaximize?: () => void;
|
||||||
document?: Document | null;
|
document?: Document | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type NaturalSize = { width: number | null; height: number | null };
|
|
||||||
type FocusPoint = { xRatio: number; yRatio: number } | null;
|
|
||||||
type DisplayKind = 'image' | 'pdf';
|
type DisplayKind = 'image' | 'pdf';
|
||||||
|
|
||||||
type DocumentLink = {
|
const determineDisplayKind = (doc?: Document | null): DisplayKind => {
|
||||||
url: string;
|
const type = doc?.mime_type?.toLowerCase?.() || '';
|
||||||
alt?: string;
|
|
||||||
mimeType?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DocumentWithPreview = Document & { documentLink?: DocumentLink };
|
|
||||||
|
|
||||||
const determineDisplayKind = (entry?: DocumentLink | null): DisplayKind => {
|
|
||||||
const type = entry?.mimeType?.toLowerCase?.() || '';
|
|
||||||
if (type.includes('pdf')) {
|
if (type.includes('pdf')) {
|
||||||
return 'pdf';
|
return 'pdf';
|
||||||
}
|
}
|
||||||
if (type.startsWith('image/')) {
|
|
||||||
return 'image';
|
|
||||||
}
|
|
||||||
const url = entry?.url?.toLowerCase?.() || '';
|
|
||||||
if (url.endsWith('.pdf')) {
|
|
||||||
return 'pdf';
|
|
||||||
}
|
|
||||||
return 'image';
|
return 'image';
|
||||||
};
|
};
|
||||||
|
|
||||||
const noop = () => { };
|
|
||||||
|
|
||||||
const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
||||||
open = false,
|
open = false,
|
||||||
onClose = noop,
|
onClose,
|
||||||
onMaximize,
|
onMaximize,
|
||||||
document: overlayDocument = null,
|
document: inputDocument = null,
|
||||||
}) => {
|
}) => {
|
||||||
const portalTarget = document.body;
|
|
||||||
const [isNativeScale, setIsNativeScale] = useState(false);
|
|
||||||
const [naturalSize, setNaturalSize] = useState<NaturalSize>({ width: null, height: null });
|
|
||||||
const [renderBackdrop, setRenderBackdrop] = useState(false);
|
const [renderBackdrop, setRenderBackdrop] = useState(false);
|
||||||
const [isBackdropVisible, setBackdropVisible] = useState(false);
|
const [isBackdropVisible, setBackdropVisible] = useState(false);
|
||||||
const [documentSnapshot, setDocumentSnapshot] = useState<DocumentWithPreview | null>(null);
|
const lastDocumentRef = useRef<Document | null>(null);
|
||||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||||
const mediaRef = useRef<HTMLImageElement | null>(null);
|
const timerRef = useRef<number | null>(null);
|
||||||
const focusRef = useRef<FocusPoint>(null);
|
|
||||||
const previouslyFocusedRef = useRef<HTMLElement | null>(null);
|
|
||||||
const visibilityTimerRef = useRef<number | null>(null);
|
|
||||||
const displayTimerRef = useRef<number | null>(null);
|
|
||||||
|
|
||||||
const currentDocument = overlayDocument;
|
if (inputDocument) {
|
||||||
|
lastDocumentRef.current = inputDocument;
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadUrl = lastDocumentRef.current?.current_version?.download?.url;
|
||||||
|
const displayKind = determineDisplayKind(lastDocumentRef.current);
|
||||||
|
const documentTitle = lastDocumentRef.current?.title || undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentDocument?.documentLink?.url) {
|
if (timerRef.current) {
|
||||||
setDocumentSnapshot(currentDocument);
|
clearTimeout(timerRef.current);
|
||||||
}
|
cancelAnimationFrame(timerRef.current);
|
||||||
}, [currentDocument]);
|
timerRef.current = null;
|
||||||
|
|
||||||
const activeDocument = open && currentDocument?.documentLink?.url ? currentDocument : documentSnapshot;
|
|
||||||
const documentLink = activeDocument?.documentLink || null;
|
|
||||||
const displayKind = useMemo(() => determineDisplayKind(documentLink), [documentLink]);
|
|
||||||
const isPdfDisplay = displayKind === 'pdf';
|
|
||||||
const documentTitle = activeDocument?.title || undefined;
|
|
||||||
const effectiveAlt = documentLink?.alt || documentTitle || 'Document preview';
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (visibilityTimerRef.current) {
|
|
||||||
clearTimeout(visibilityTimerRef.current);
|
|
||||||
visibilityTimerRef.current = null;
|
|
||||||
}
|
|
||||||
if (displayTimerRef.current) {
|
|
||||||
cancelAnimationFrame(displayTimerRef.current);
|
|
||||||
displayTimerRef.current = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (open && documentLink?.url) {
|
if (open) {
|
||||||
setRenderBackdrop(true);
|
setRenderBackdrop(true);
|
||||||
displayTimerRef.current = requestAnimationFrame(() => {
|
timerRef.current = requestAnimationFrame(() => {
|
||||||
displayTimerRef.current = requestAnimationFrame(() => {
|
timerRef.current = requestAnimationFrame(() => {
|
||||||
setBackdropVisible(true);
|
setBackdropVisible(true);
|
||||||
});
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
if (displayTimerRef.current) {
|
|
||||||
cancelAnimationFrame(displayTimerRef.current);
|
|
||||||
displayTimerRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
setBackdropVisible(false);
|
|
||||||
visibilityTimerRef.current = window.setTimeout(() => {
|
|
||||||
setRenderBackdrop(false);
|
|
||||||
}, 260);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (visibilityTimerRef.current) {
|
|
||||||
clearTimeout(visibilityTimerRef.current);
|
|
||||||
visibilityTimerRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [open, documentLink?.url]);
|
|
||||||
|
|
||||||
useEffect(() => () => {
|
|
||||||
if (visibilityTimerRef.current) {
|
|
||||||
clearTimeout(visibilityTimerRef.current);
|
|
||||||
}
|
|
||||||
if (displayTimerRef.current) {
|
|
||||||
cancelAnimationFrame(displayTimerRef.current);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
}, [isPdfDisplay, open, documentLink?.url]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsNativeScale(false);
|
|
||||||
setNaturalSize({ width: null, height: null });
|
|
||||||
focusRef.current = null;
|
|
||||||
const scrollEl = scrollRef.current;
|
|
||||||
if (scrollEl) {
|
|
||||||
scrollEl.scrollLeft = 0;
|
|
||||||
scrollEl.scrollTop = 0;
|
|
||||||
}
|
|
||||||
}, [open, documentLink?.url, displayKind]);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open || !isNativeScale) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollEl = scrollRef.current;
|
|
||||||
const mediaEl = mediaRef.current;
|
|
||||||
if (!scrollEl || !mediaEl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageWidth = naturalSize.width || mediaEl.clientWidth;
|
|
||||||
const imageHeight = naturalSize.height || mediaEl.clientHeight;
|
|
||||||
if (!(imageWidth > 0 && imageHeight > 0)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = focusRef.current || { xRatio: 0.5, yRatio: 0.5 };
|
|
||||||
const maxScrollLeft = Math.max(0, imageWidth - scrollEl.clientWidth);
|
|
||||||
const maxScrollTop = Math.max(0, imageHeight - scrollEl.clientHeight);
|
|
||||||
|
|
||||||
const desiredLeft = target.xRatio * imageWidth - scrollEl.clientWidth / 2;
|
|
||||||
const desiredTop = target.yRatio * imageHeight - scrollEl.clientHeight / 2;
|
|
||||||
|
|
||||||
scrollEl.scrollLeft = clamp(desiredLeft, 0, maxScrollLeft);
|
|
||||||
scrollEl.scrollTop = clamp(desiredTop, 0, maxScrollTop);
|
|
||||||
}, [open, isNativeScale, naturalSize.width, naturalSize.height]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) {
|
|
||||||
previouslyFocusedRef.current?.focus?.();
|
|
||||||
previouslyFocusedRef.current = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const active = document.activeElement;
|
|
||||||
previouslyFocusedRef.current = active instanceof HTMLElement ? active : null;
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!documentLink?.url) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollEl = scrollRef.current;
|
|
||||||
if (scrollEl) {
|
|
||||||
scrollEl.scrollLeft = 0;
|
|
||||||
scrollEl.scrollTop = 0;
|
|
||||||
}
|
|
||||||
focusRef.current = null;
|
|
||||||
}, [documentLink?.url]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!renderBackdrop || !documentLink?.url) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const frame = requestAnimationFrame(() => {
|
|
||||||
scrollRef.current?.focus?.({ preventScroll: true });
|
scrollRef.current?.focus?.({ preventScroll: true });
|
||||||
|
timerRef.current = null;
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setBackdropVisible(false);
|
||||||
|
timerRef.current = window.setTimeout(() => {
|
||||||
|
setRenderBackdrop(false);
|
||||||
|
timerRef.current = null;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
return () => cancelAnimationFrame(frame);
|
return () => {
|
||||||
}, [renderBackdrop, documentLink?.url]);
|
if (timerRef.current) {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
cancelAnimationFrame(timerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -213,59 +85,15 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
|||||||
|
|
||||||
const key = event.key;
|
const key = event.key;
|
||||||
|
|
||||||
if (key === ' ' || key === 'Space' || key === 'Spacebar') {
|
if (key === ' ' || key === 'Escape') {
|
||||||
const target = event.target;
|
|
||||||
if (target instanceof HTMLElement) {
|
|
||||||
const tag = target.tagName ? target.tagName.toLowerCase() : '';
|
|
||||||
if (
|
|
||||||
target.isContentEditable
|
|
||||||
|| tag === 'input'
|
|
||||||
|| tag === 'textarea'
|
|
||||||
|| tag === 'select'
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onClose();
|
onClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === 'Escape') {
|
|
||||||
event.preventDefault();
|
|
||||||
onClose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleZoomAtPoint = (_clientX: number, _clientY: number) => {
|
const stageClassName = 'preview-zoom__stage';
|
||||||
// Zooming is disabled for images as per user request.
|
const containerClassName = 'preview-zoom__scroll';
|
||||||
// PDFs handle their own zooming via the PdfViewer.
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleContentClick = (event: React.MouseEvent<HTMLElement>) => {
|
|
||||||
if (isPdfDisplay) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.stopPropagation();
|
|
||||||
toggleZoomAtPoint(event.clientX ?? 0, event.clientY ?? 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const shouldRender = renderBackdrop && Boolean(documentLink?.url);
|
|
||||||
|
|
||||||
const stageClassName = isPdfDisplay
|
|
||||||
? 'preview-zoom__stage preview-zoom__stage--pdf'
|
|
||||||
: 'preview-zoom__stage';
|
|
||||||
|
|
||||||
const containerClassName = [
|
|
||||||
'preview-zoom__scroll',
|
|
||||||
isNativeScale ? 'preview-zoom__scroll--native' : null,
|
|
||||||
isPdfDisplay ? 'preview-zoom__scroll--pdf' : null,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ');
|
|
||||||
|
|
||||||
const backdropClassName = [
|
const backdropClassName = [
|
||||||
'preview-zoom-backdrop',
|
'preview-zoom-backdrop',
|
||||||
@@ -274,30 +102,10 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
|
|
||||||
const contentStyle: CSSProperties = isNativeScale
|
if (!renderBackdrop) {
|
||||||
? {
|
|
||||||
cursor: 'default',
|
|
||||||
width: naturalSize.width ? `${naturalSize.width}px` : 'auto',
|
|
||||||
height: naturalSize.height ? `${naturalSize.height}px` : 'auto',
|
|
||||||
maxWidth: 'none',
|
|
||||||
maxHeight: 'none',
|
|
||||||
touchAction: 'manipulation',
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
cursor: 'default',
|
|
||||||
maxWidth: 'calc(100vw - 2 * var(--preview-padding, 1.5vmin))',
|
|
||||||
maxHeight: 'calc(100vh - 2 * var(--preview-padding, 1.5vmin))',
|
|
||||||
touchAction: 'manipulation',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!shouldRender) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const effectiveDisplay = documentLink;
|
|
||||||
|
|
||||||
const downloadUrl = activeDocument?.current_version?.download?.url;
|
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
(
|
(
|
||||||
<div
|
<div
|
||||||
@@ -364,41 +172,28 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
|
|||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
{isPdfDisplay ? (
|
{displayKind === 'pdf' ? (
|
||||||
<div className="preview-zoom__pdf">
|
|
||||||
<PdfViewer
|
<PdfViewer
|
||||||
src={effectiveDisplay.url}
|
src={downloadUrl}
|
||||||
title={effectiveAlt}
|
title={documentTitle}
|
||||||
className="preview-zoom__pdf-viewer"
|
className="preview-zoom__pdf-viewer"
|
||||||
viewportRef={scrollRef}
|
viewportRef={scrollRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<MediaViewer
|
<MediaViewer
|
||||||
src={effectiveDisplay.url}
|
src={downloadUrl}
|
||||||
alt={effectiveAlt}
|
alt={documentTitle}
|
||||||
className="preview-zoom__image"
|
className="preview-zoom__image"
|
||||||
mediaRef={(node: HTMLElement | null) => {
|
|
||||||
mediaRef.current = node as HTMLImageElement;
|
|
||||||
}}
|
|
||||||
draggable={false}
|
draggable={false}
|
||||||
onLoad={(event) => {
|
onClick={(e) => e.stopPropagation()}
|
||||||
const target = event.currentTarget as HTMLImageElement;
|
mimeType={lastDocumentRef.current?.mime_type || undefined}
|
||||||
setNaturalSize({
|
|
||||||
width: target.naturalWidth || null,
|
|
||||||
height: target.naturalHeight || null,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onClick={handleContentClick}
|
|
||||||
style={contentStyle}
|
|
||||||
mimeType={effectiveDisplay.mimeType || undefined}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
portalTarget,
|
document.body,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { createContext, useContext, useState, useCallback, useMemo, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useCallback, useMemo, useRef } from 'react';
|
||||||
import PreviewZoomOverlay from '../detail/PreviewZoomOverlay';
|
import PreviewZoomOverlay from '../detail/PreviewZoomOverlay';
|
||||||
import type { Document } from '../types/documents';
|
import type { Document } from '../types/documents';
|
||||||
import type { Identifier } from '../types/identifiers';
|
import type { Identifier } from '../types/identifiers';
|
||||||
@@ -20,21 +20,26 @@ export const usePreviewContext = () => {
|
|||||||
|
|
||||||
interface PreviewProviderProps {
|
interface PreviewProviderProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
ensureDownloadUrl?: (docId: Identifier) => Promise<any>;
|
|
||||||
onNavigate?: (documentId: Identifier) => void;
|
onNavigate?: (documentId: Identifier) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PreviewProvider: React.FC<PreviewProviderProps> = ({ children, ensureDownloadUrl, onNavigate }) => {
|
export const PreviewProvider: React.FC<PreviewProviderProps> = ({ children, onNavigate }) => {
|
||||||
const [previewDoc, setPreviewDoc] = useState<Document | null>(null);
|
const [previewDoc, setPreviewDoc] = useState<Document | null>(null);
|
||||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
const lastFocusedElement = useRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
const openPreview = useCallback((doc: Document) => {
|
const openPreview = useCallback((doc: Document) => {
|
||||||
|
if (!lastFocusedElement.current) {
|
||||||
|
lastFocusedElement.current = document.activeElement as HTMLElement;
|
||||||
|
}
|
||||||
setPreviewDoc(doc);
|
setPreviewDoc(doc);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const closePreview = useCallback(() => {
|
const closePreview = useCallback(() => {
|
||||||
setPreviewDoc(null);
|
setPreviewDoc(null);
|
||||||
setPreviewUrl(null);
|
if (lastFocusedElement.current) {
|
||||||
|
lastFocusedElement.current.focus();
|
||||||
|
lastFocusedElement.current = null;
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleMaximize = useCallback(() => {
|
const handleMaximize = useCallback(() => {
|
||||||
@@ -44,46 +49,6 @@ export const PreviewProvider: React.FC<PreviewProviderProps> = ({ children, ensu
|
|||||||
}
|
}
|
||||||
}, [previewDoc, onNavigate, closePreview]);
|
}, [previewDoc, onNavigate, closePreview]);
|
||||||
|
|
||||||
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(() => ({
|
const value = useMemo(() => ({
|
||||||
openPreview,
|
openPreview,
|
||||||
closePreview,
|
closePreview,
|
||||||
@@ -93,10 +58,10 @@ export const PreviewProvider: React.FC<PreviewProviderProps> = ({ children, ensu
|
|||||||
<PreviewContext.Provider value={value}>
|
<PreviewContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
<PreviewZoomOverlay
|
<PreviewZoomOverlay
|
||||||
open={Boolean(previewUrl)}
|
open={Boolean(previewDoc)}
|
||||||
onClose={closePreview}
|
onClose={closePreview}
|
||||||
onMaximize={onNavigate ? handleMaximize : undefined}
|
onMaximize={onNavigate ? handleMaximize : undefined}
|
||||||
document={overlayDocument}
|
document={previewDoc}
|
||||||
/>
|
/>
|
||||||
</PreviewContext.Provider>
|
</PreviewContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -616,8 +616,6 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.document-summary__details {}
|
|
||||||
|
|
||||||
.document-summary__details-list {
|
.document-summary__details-list {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -26,37 +26,32 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 6000001;
|
z-index: 6000001;
|
||||||
--preview-padding: 1.5vmin;
|
|
||||||
--header-clearance: calc(2.5rem + 0.5rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-zoom__stage--pdf {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
--preview-padding: 1.5vmin;
|
||||||
|
--header-clearance: calc(2.5rem + 0.5rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-zoom__image {
|
.preview-zoom__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);
|
||||||
|
cursor: default;
|
||||||
|
max-width: calc(100vw - 2 * var(--preview-padding, 1.5vmin));
|
||||||
|
max-height: calc(100vh - var(--header-clearance) - 2 * var(--preview-padding, 1.5vmin));
|
||||||
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-zoom__scroll {
|
.preview-zoom__scroll {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-top: var(--header-clearance);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-zoom__scroll--pdf {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
padding-top: var(--header-clearance);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: center;
|
|
||||||
--pdf-viewer-stack-padding: var(--preview-padding);
|
--pdf-viewer-stack-padding: var(--preview-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,25 +59,8 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-zoom__scroll--native {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
cursor: zoom-out;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.preview-zoom__pdf {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: flex-start;
|
|
||||||
padding-top: var(--header-clearance);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-zoom__pdf-viewer {
|
.preview-zoom__pdf-viewer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Identifier } from './identifiers';
|
import type { Identifier } from './identifiers';
|
||||||
|
import type { Download } from './common';
|
||||||
|
|
||||||
export interface AssetObject {
|
export interface AssetObject {
|
||||||
ordinal?: number;
|
ordinal?: number;
|
||||||
@@ -12,7 +13,7 @@ export interface AssetLike {
|
|||||||
id?: Identifier;
|
id?: Identifier;
|
||||||
asset_type?: string;
|
asset_type?: string;
|
||||||
cardinality?: number | null;
|
cardinality?: number | null;
|
||||||
download?: { url: string; expires_at: number } | null;
|
download?: Download | null;
|
||||||
metadata?: Record<string, unknown> | null;
|
metadata?: Record<string, unknown> | null;
|
||||||
assets?: Record<string, AssetLike> | AssetLike[] | null;
|
assets?: Record<string, AssetLike> | AssetLike[] | null;
|
||||||
objects?: AssetObject[] | null;
|
objects?: AssetObject[] | null;
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface Download {
|
||||||
|
url: string;
|
||||||
|
expires_at: number;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Identifier } from './identifiers';
|
import type { Identifier } from './identifiers';
|
||||||
import type { AssetLike } from './assets';
|
import type { AssetLike } from './assets';
|
||||||
|
import type { Download } from './common';
|
||||||
|
|
||||||
export interface DocumentTag {
|
export interface DocumentTag {
|
||||||
id?: Identifier;
|
id?: Identifier;
|
||||||
@@ -18,6 +19,7 @@ export interface DocumentVersion {
|
|||||||
metadata?: Record<string, unknown> & { page_count?: number } | null;
|
metadata?: Record<string, unknown> & { page_count?: number } | null;
|
||||||
size_bytes?: number | string | null;
|
size_bytes?: number | string | null;
|
||||||
checksum?: string | null;
|
checksum?: string | null;
|
||||||
|
download?: Download | null;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user