This commit is contained in:
2025-11-11 10:09:15 +01:00
parent 3fe325c4ce
commit 6ae497323e
3 changed files with 182 additions and 29 deletions
-2
View File
@@ -83,7 +83,6 @@ export const useWorkspaceSurface = ({
onFolderNavigate,
} = detailExtras;
return createDocumentViewerSurface({
documentId: previewDocumentId,
document: previewWorkspaceDocument,
previewEntry: previewWorkspaceEntry,
ensureAssetUrl,
@@ -108,7 +107,6 @@ export const useWorkspaceSurface = ({
}, [
showPreviewWorkspace,
previewWorkspaceDocument,
previewDocumentId,
previewWorkspaceEntry,
ensureAssetUrl,
ensurePreviewData,
+140 -23
View File
@@ -1,4 +1,11 @@
import React, { useCallback, useMemo } from 'react';
import React, {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import { DownloadIcon, CloseIcon } from '../ui/icons';
import {
buildCorrespondentOptions,
@@ -9,10 +16,50 @@ import { extractDocumentMetadataPayload } from '../documents/documentMetadata';
import { createDocumentActionState } from '../documents/documentActions';
import { resolveDocumentAssetUrl } from '../asset_manager';
const PORTRAIT_WIDTH_TO_HEIGHT = 1 / Math.sqrt(2); // ≈0.707 (A-series aspect ratio)
const DOCUMENT_VIEWER_PORTRAIT_HEIGHT_RATIO = 0.6;
const PORTRAIT_RATIO_STYLE_ID = 'document-viewer-portrait-ratio-style';
const ensurePortraitRatioStyle = () => {
if (typeof document === 'undefined') {
return;
}
const cssValue = String(DOCUMENT_VIEWER_PORTRAIT_HEIGHT_RATIO);
const cssText = `:root { --document-viewer-portrait-height-ratio: ${cssValue}; }`;
let styleEl = document.getElementById(PORTRAIT_RATIO_STYLE_ID);
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = PORTRAIT_RATIO_STYLE_ID;
document.head.appendChild(styleEl);
}
if (styleEl.textContent !== cssText) {
styleEl.textContent = cssText;
}
};
if (typeof document !== 'undefined') {
ensurePortraitRatioStyle();
}
const computeStackedLayoutBreakpoint = () => {
const viewportHeight = typeof window !== 'undefined' ? window.innerHeight : 900;
const portraitViewportWidth = viewportHeight
* DOCUMENT_VIEWER_PORTRAIT_HEIGHT_RATIO
* PORTRAIT_WIDTH_TO_HEIGHT;
const detailsColumnWidth = 320; // px ~ 20rem for metadata & tabs
const gutterAllowance = 48; // padding + grid gap
const desiredWidth = portraitViewportWidth + detailsColumnWidth + gutterAllowance;
return desiredWidth;
};
const DocumentViewerPanel = ({
document,
documentId,
previewEntry,
hydrateDocument,
tagLookupById,
tagOptions,
onTagAdd,
@@ -182,13 +229,81 @@ const DocumentViewerPanel = ({
[hasOcr, loadOcrContent],
);
const viewerRef = useRef(null);
const [isStackedLayout, setIsStackedLayout] = useState(false);
useEffect(() => {
if (typeof hydrateDocument === 'function' && document?.id) {
hydrateDocument(document.id);
}
}, [hydrateDocument, document?.id]);
useLayoutEffect(() => {
ensurePortraitRatioStyle();
}, []);
useLayoutEffect(() => {
const node = viewerRef.current;
if (!node) {
setIsStackedLayout(false);
return undefined;
}
let frame = null;
const commitMeasure = (width) => {
if (frame) {
cancelAnimationFrame(frame);
}
frame = requestAnimationFrame(() => {
const breakpoint = computeStackedLayoutBreakpoint();
setIsStackedLayout(width < breakpoint);
});
};
const measure = () => {
commitMeasure(node.getBoundingClientRect().width);
};
measure();
if (typeof ResizeObserver === 'undefined') {
window.addEventListener('resize', measure);
return () => {
if (frame) {
cancelAnimationFrame(frame);
}
window.removeEventListener('resize', measure);
};
}
const observer = new ResizeObserver((entries) => {
if (!entries.length) {
return;
}
commitMeasure(entries[0].contentRect.width);
});
observer.observe(node);
return () => {
observer.disconnect();
if (frame) {
cancelAnimationFrame(frame);
}
};
}, [document?.id]);
const viewerClassName = isStackedLayout
? 'document-viewer document-viewer--stacked'
: 'document-viewer';
if (!document) {
return (
<section className="document-viewer document-viewer--loading">
<section className="document-viewer document-viewer--loading" ref={viewerRef}>
<div className="document-viewer__details-pane">
<div className="document-viewer__details">
<div className="document-viewer__message">
Loading document{documentId ? ` ${documentId}` : ''}
Loading document
</div>
</div>
</div>
@@ -200,7 +315,7 @@ const DocumentViewerPanel = ({
}
return (
<section className="document-viewer">
<section className={viewerClassName} ref={viewerRef}>
<div className="document-viewer__details-pane">
<div className="document-viewer__details">
<DocumentInfoPanel
@@ -231,12 +346,16 @@ export default DocumentViewerPanel;
export const createDocumentViewerHeaderActions = ({
document,
actionState,
previewEntry,
}) => {
if (!document || !actionState) {
if (!document) {
return null;
}
const { downloadHref } = actionState;
const downloadHref = actionState?.downloadHref || previewEntry?.url;
if (!downloadHref) {
return null;
}
return (
<>
@@ -257,7 +376,6 @@ export const createDocumentViewerHeaderActions = ({
};
export const createDocumentViewerSurface = ({
documentId,
document,
previewEntry,
ensureAssetUrl,
@@ -278,11 +396,11 @@ export const createDocumentViewerSurface = ({
onUpdateIssued,
resolveFolderPath,
}) => {
if (!documentId && !document) {
if (!document) {
return null;
}
const title = document?.title || 'Document preview';
const title = document.title || 'Document preview';
const closeButton = onClose
? (
<button
@@ -306,7 +424,7 @@ export const createDocumentViewerSurface = ({
)
: null;
let breadcrumbs = null;
if (document && typeof resolveFolderPath === 'function') {
if (typeof resolveFolderPath === 'function') {
const folderSegments = resolveFolderPath(document.folder_id);
const normalizedSegments = Array.isArray(folderSegments)
? folderSegments
@@ -320,17 +438,15 @@ export const createDocumentViewerSurface = ({
];
}
const actionState = document
? createDocumentActionState({
document,
resolveApiPath,
ensurePreviewData,
ensureAssetUrl,
getDocumentAsset,
notifyApiError,
ocrErrorMessage: 'Unable to open OCR text.',
})
: null;
const actionState = createDocumentActionState({
document,
resolveApiPath,
ensurePreviewData,
ensureAssetUrl,
getDocumentAsset,
notifyApiError,
ocrErrorMessage: 'Unable to open OCR text.',
});
const header = {
title,
@@ -339,6 +455,7 @@ export const createDocumentViewerSurface = ({
actions: createDocumentViewerHeaderActions({
document,
actionState,
previewEntry,
}),
breadcrumbs,
};
@@ -350,8 +467,8 @@ export const createDocumentViewerSurface = ({
content: (
<DocumentViewerPanel
document={document}
documentId={documentId}
previewEntry={previewEntry}
hydrateDocument={ensurePreviewData}
tagLookupById={tagLookupById}
tagOptions={tagOptions}
onTagAdd={onTagAdd}
+42 -4
View File
@@ -1,12 +1,42 @@
.document-viewer {
flex: 1;
display: grid;
grid-template-columns: minmax(0, 30em) minmax(0, 1fr);
grid-template-columns: minmax(0, clamp(18rem, 30vw, 26rem)) minmax(0, 1fr);
grid-template-areas: 'details viewport';
align-items: stretch;
gap: 1rem;
min-height: 0;
padding: 1rem 1rem;
}
.document-viewer--stacked {
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-rows:
minmax(auto, 1fr)
auto;
grid-template-areas:
'viewport'
'details';
gap: 0.5rem;
padding: 0.5rem;
overflow: auto;
}
.document-viewer--stacked .document-viewer__viewport {
width: 100%;
margin: 0;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
max-height: calc(var(--document-viewer-portrait-height-ratio) * 100vh);
}
.document-viewer--stacked .document-viewer__details-pane {
overflow: visible;
}
.document-drag-preview {
position: fixed;
pointer-events: none;
@@ -125,6 +155,7 @@
min-height: 0;
overflow: auto;
flex: 1;
grid-area: details;
}
.document-viewer__tabs-wrapper {
display: flex;
@@ -289,22 +320,29 @@
display: flex;
position: relative;
overflow: hidden;
align-items: flex-start;
align-items: stretch;
justify-content: center;
grid-area: viewport;
}
.document-viewer__object {
width: 100%;
height: 100%;
border: none;
}
.document-viewer__object:not(.document-viewer__object--image) {
height: 100%;
}
.document-viewer__object--image {
width: auto;
height: auto;
max-width: 100%;
max-height: 100%;
object-fit: contain;
align-self: flex-start;
}
.document-viewer__unsupported {