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, onFolderNavigate,
} = detailExtras; } = detailExtras;
return createDocumentViewerSurface({ return createDocumentViewerSurface({
documentId: previewDocumentId,
document: previewWorkspaceDocument, document: previewWorkspaceDocument,
previewEntry: previewWorkspaceEntry, previewEntry: previewWorkspaceEntry,
ensureAssetUrl, ensureAssetUrl,
@@ -108,7 +107,6 @@ export const useWorkspaceSurface = ({
}, [ }, [
showPreviewWorkspace, showPreviewWorkspace,
previewWorkspaceDocument, previewWorkspaceDocument,
previewDocumentId,
previewWorkspaceEntry, previewWorkspaceEntry,
ensureAssetUrl, ensureAssetUrl,
ensurePreviewData, 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 { DownloadIcon, CloseIcon } from '../ui/icons';
import { import {
buildCorrespondentOptions, buildCorrespondentOptions,
@@ -9,10 +16,50 @@ import { extractDocumentMetadataPayload } from '../documents/documentMetadata';
import { createDocumentActionState } from '../documents/documentActions'; import { createDocumentActionState } from '../documents/documentActions';
import { resolveDocumentAssetUrl } from '../asset_manager'; 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 = ({ const DocumentViewerPanel = ({
document, document,
documentId,
previewEntry, previewEntry,
hydrateDocument,
tagLookupById, tagLookupById,
tagOptions, tagOptions,
onTagAdd, onTagAdd,
@@ -182,13 +229,81 @@ const DocumentViewerPanel = ({
[hasOcr, loadOcrContent], [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) { if (!document) {
return ( 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-pane">
<div className="document-viewer__details"> <div className="document-viewer__details">
<div className="document-viewer__message"> <div className="document-viewer__message">
Loading document{documentId ? ` ${documentId}` : ''} Loading document
</div> </div>
</div> </div>
</div> </div>
@@ -200,7 +315,7 @@ const DocumentViewerPanel = ({
} }
return ( return (
<section className="document-viewer"> <section className={viewerClassName} ref={viewerRef}>
<div className="document-viewer__details-pane"> <div className="document-viewer__details-pane">
<div className="document-viewer__details"> <div className="document-viewer__details">
<DocumentInfoPanel <DocumentInfoPanel
@@ -231,12 +346,16 @@ export default DocumentViewerPanel;
export const createDocumentViewerHeaderActions = ({ export const createDocumentViewerHeaderActions = ({
document, document,
actionState, actionState,
previewEntry,
}) => { }) => {
if (!document || !actionState) { if (!document) {
return null; return null;
} }
const { downloadHref } = actionState; const downloadHref = actionState?.downloadHref || previewEntry?.url;
if (!downloadHref) {
return null;
}
return ( return (
<> <>
@@ -257,7 +376,6 @@ export const createDocumentViewerHeaderActions = ({
}; };
export const createDocumentViewerSurface = ({ export const createDocumentViewerSurface = ({
documentId,
document, document,
previewEntry, previewEntry,
ensureAssetUrl, ensureAssetUrl,
@@ -278,11 +396,11 @@ export const createDocumentViewerSurface = ({
onUpdateIssued, onUpdateIssued,
resolveFolderPath, resolveFolderPath,
}) => { }) => {
if (!documentId && !document) { if (!document) {
return null; return null;
} }
const title = document?.title || 'Document preview'; const title = document.title || 'Document preview';
const closeButton = onClose const closeButton = onClose
? ( ? (
<button <button
@@ -306,7 +424,7 @@ export const createDocumentViewerSurface = ({
) )
: null; : null;
let breadcrumbs = null; let breadcrumbs = null;
if (document && typeof resolveFolderPath === 'function') { if (typeof resolveFolderPath === 'function') {
const folderSegments = resolveFolderPath(document.folder_id); const folderSegments = resolveFolderPath(document.folder_id);
const normalizedSegments = Array.isArray(folderSegments) const normalizedSegments = Array.isArray(folderSegments)
? folderSegments ? folderSegments
@@ -320,17 +438,15 @@ export const createDocumentViewerSurface = ({
]; ];
} }
const actionState = document const actionState = createDocumentActionState({
? createDocumentActionState({ document,
document, resolveApiPath,
resolveApiPath, ensurePreviewData,
ensurePreviewData, ensureAssetUrl,
ensureAssetUrl, getDocumentAsset,
getDocumentAsset, notifyApiError,
notifyApiError, ocrErrorMessage: 'Unable to open OCR text.',
ocrErrorMessage: 'Unable to open OCR text.', });
})
: null;
const header = { const header = {
title, title,
@@ -339,6 +455,7 @@ export const createDocumentViewerSurface = ({
actions: createDocumentViewerHeaderActions({ actions: createDocumentViewerHeaderActions({
document, document,
actionState, actionState,
previewEntry,
}), }),
breadcrumbs, breadcrumbs,
}; };
@@ -350,8 +467,8 @@ export const createDocumentViewerSurface = ({
content: ( content: (
<DocumentViewerPanel <DocumentViewerPanel
document={document} document={document}
documentId={documentId}
previewEntry={previewEntry} previewEntry={previewEntry}
hydrateDocument={ensurePreviewData}
tagLookupById={tagLookupById} tagLookupById={tagLookupById}
tagOptions={tagOptions} tagOptions={tagOptions}
onTagAdd={onTagAdd} onTagAdd={onTagAdd}
+42 -4
View File
@@ -1,12 +1,42 @@
.document-viewer { .document-viewer {
flex: 1; flex: 1;
display: grid; 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; gap: 1rem;
min-height: 0; min-height: 0;
padding: 1rem 1rem; 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 { .document-drag-preview {
position: fixed; position: fixed;
pointer-events: none; pointer-events: none;
@@ -125,6 +155,7 @@
min-height: 0; min-height: 0;
overflow: auto; overflow: auto;
flex: 1; flex: 1;
grid-area: details;
} }
.document-viewer__tabs-wrapper { .document-viewer__tabs-wrapper {
display: flex; display: flex;
@@ -289,22 +320,29 @@
display: flex; display: flex;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
align-items: flex-start; align-items: stretch;
justify-content: center;
grid-area: viewport;
} }
.document-viewer__object { .document-viewer__object {
width: 100%; width: 100%;
height: 100%;
border: none; border: none;
} }
.document-viewer__object:not(.document-viewer__object--image) {
height: 100%;
}
.document-viewer__object--image { .document-viewer__object--image {
width: auto; width: auto;
height: auto; height: auto;
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
object-fit: contain; object-fit: contain;
align-self: flex-start;
} }
.document-viewer__unsupported { .document-viewer__unsupported {