From 1f2c62241cb63da4a10e5b8af6a42d0f0fee590e Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Sun, 16 Nov 2025 21:08:07 +0100 Subject: [PATCH] cleanup --- frontend/src/desktop/DesktopWorkspace.tsx | 17 +++ frontend/src/detail/PreviewZoomOverlay.tsx | 125 +++++++----------- .../src/documents/panel/DocumentsPanel.tsx | 7 +- frontend/src/preview/DocumentViewerPanel.tsx | 19 ++- frontend/src/preview/PdfViewer.tsx | 2 - frontend/src/styles/preview/preview-zoom.css | 54 -------- 6 files changed, 82 insertions(+), 142 deletions(-) diff --git a/frontend/src/desktop/DesktopWorkspace.tsx b/frontend/src/desktop/DesktopWorkspace.tsx index 95ce53a..8f58d04 100644 --- a/frontend/src/desktop/DesktopWorkspace.tsx +++ b/frontend/src/desktop/DesktopWorkspace.tsx @@ -39,6 +39,7 @@ export interface DeskDocument { id?: Identifier | null; title?: string; tags?: TagLike[] | null; + previewEntry?: OverlaySource | null; [key: string]: unknown; } @@ -172,6 +173,7 @@ interface DesktopWorkspaceViewProps { closeOverlay: () => void; overlayOriginRect: DOMRect | null; overlayOriginTransform: OverlayOriginTransform | null; + overlayDocument: DeskDocument | null; onEntryPointer?: DesktopWorkspaceProps['onEntryPointer']; onDocumentStackSelect?: DesktopWorkspaceProps['onDocumentStackSelect']; onPromoteSelection?: DesktopWorkspaceProps['onPromoteSelection']; @@ -677,6 +679,17 @@ const DesktopWorkspace: React.FC = ({ }; }, [overlaySource]); + const overlayDocument = useMemo(() => { + if (!overlayDocId) { + return null; + } + const baseDoc = documentLookup.get(String(overlayDocId)) || null; + if (baseDoc && overlayDisplay?.url) { + return { ...baseDoc, previewEntry: overlayDisplay }; + } + return baseDoc; + }, [documentLookup, overlayDisplay, overlayDocId]); + const openOverlayForDoc = useCallback( (docId: Identifier | null, originInfo: OverlayOriginHint | null = null) => { if (!docId) { @@ -782,6 +795,7 @@ const DesktopWorkspace: React.FC = ({ closeOverlay, overlayOriginRect, overlayOriginTransform, + overlayDocument, onEntryPointer, onDocumentStackSelect, onPromoteSelection, @@ -838,6 +852,7 @@ const DesktopWorkspace: React.FC = ({ overlayDisplay, overlayOriginRect, overlayOriginTransform, + overlayDocument, documentLookup, pendingRemovalTag, pendingTagDocId, @@ -887,6 +902,7 @@ function DesktopWorkspaceView({ closeOverlay, overlayOriginRect, overlayOriginTransform, + overlayDocument, onEntryPointer, onDocumentStackSelect, onPromoteSelection, @@ -1108,6 +1124,7 @@ function DesktopWorkspaceView({ open={Boolean(overlayDisplay?.url)} display={overlayDisplay} onClose={closeOverlay} + document={overlayDocument} originRect={overlayOriginRect} originTransform={overlayOriginTransform} /> diff --git a/frontend/src/detail/PreviewZoomOverlay.tsx b/frontend/src/detail/PreviewZoomOverlay.tsx index ff33c25..97535f8 100644 --- a/frontend/src/detail/PreviewZoomOverlay.tsx +++ b/frontend/src/detail/PreviewZoomOverlay.tsx @@ -1,31 +1,38 @@ import React, { CSSProperties, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; -import { ArrowLeftIcon, ArrowRightIcon } from '../ui/icons'; import { clamp } from '../utils/math'; import PdfViewer from '../preview/PdfViewer'; -type PreviewNavigator = { - url: string; - alt?: string; - contentType?: string | null; - pageNumber?: number | null; - canGoPrev?: boolean; - canGoNext?: boolean; - goPrev?: () => void; - goNext?: () => void; +type DocumentLike = { + id?: string | number; + title?: string; + content_type?: string | null; + [key: string]: unknown; }; interface PreviewZoomOverlayProps { open?: boolean; - display?: PreviewNavigator | null; onClose?: () => void; + document?: DocumentLike | null; } type NaturalSize = { width: number | null; height: number | null }; type FocusPoint = { xRatio: number; yRatio: number } | null; type DisplayKind = 'image' | 'pdf'; -const determineDisplayKind = (entry?: PreviewNavigator | null): DisplayKind => { +type PreviewEntry = { + url: string; + alt?: string; + contentType?: string | null; + canGoPrev?: boolean; + canGoNext?: boolean; + goPrev?: () => void; + goNext?: () => void; +}; + +type DocumentLikeWithPreview = DocumentLike & { previewEntry?: PreviewEntry }; + +const determineDisplayKind = (entry?: PreviewEntry | null): DisplayKind => { const type = entry?.contentType?.toLowerCase?.() || ''; if (type.includes('pdf')) { return 'pdf'; @@ -44,15 +51,15 @@ const noop = () => {}; const PreviewZoomOverlay: React.FC = ({ open = false, - display = null, onClose = noop, + document: overlayDocument = null, }) => { const portalTarget = document.body; const [isNativeScale, setIsNativeScale] = useState(false); const [naturalSize, setNaturalSize] = useState({ width: null, height: null }); const [renderBackdrop, setRenderBackdrop] = useState(false); const [isBackdropVisible, setBackdropVisible] = useState(false); - const [displaySnapshot, setDisplaySnapshot] = useState(null); + const [documentSnapshot, setDocumentSnapshot] = useState(null); const scrollRef = useRef(null); const mediaRef = useRef(null); const focusRef = useRef(null); @@ -60,15 +67,20 @@ const PreviewZoomOverlay: React.FC = ({ const visibilityTimerRef = useRef(null); const displayTimerRef = useRef(null); - useEffect(() => { - if (display?.url) { - setDisplaySnapshot(display); - } - }, [display]); + const currentDocument = overlayDocument as DocumentLikeWithPreview | null; - const activeDisplay = open && display?.url ? display : displaySnapshot; - const displayKind = useMemo(() => determineDisplayKind(activeDisplay), [activeDisplay]); + useEffect(() => { + if (currentDocument?.previewEntry?.url) { + setDocumentSnapshot(currentDocument); + } + }, [currentDocument]); + + const activeDocument = open && currentDocument?.previewEntry?.url ? currentDocument : documentSnapshot; + const previewEntry = activeDocument?.previewEntry || null; + const displayKind = useMemo(() => determineDisplayKind(previewEntry), [previewEntry]); const isPdfDisplay = displayKind === 'pdf'; + const documentTitle = activeDocument?.title || undefined; + const effectiveAlt = previewEntry?.alt || documentTitle || 'Document preview'; useEffect(() => { if (visibilityTimerRef.current) { @@ -80,7 +92,7 @@ const PreviewZoomOverlay: React.FC = ({ displayTimerRef.current = null; } - if (open && display?.url) { + if (open && previewEntry?.url) { setRenderBackdrop(true); displayTimerRef.current = requestAnimationFrame(() => { displayTimerRef.current = requestAnimationFrame(() => { @@ -106,7 +118,7 @@ const PreviewZoomOverlay: React.FC = ({ visibilityTimerRef.current = null; } }; - }, [open, display?.url]); + }, [open, previewEntry?.url]); useEffect(() => () => { if (visibilityTimerRef.current) { @@ -118,7 +130,7 @@ const PreviewZoomOverlay: React.FC = ({ }, []); useEffect(() => { - }, [isPdfDisplay, open, activeDisplay?.url]); + }, [isPdfDisplay, open, previewEntry?.url]); useEffect(() => { setIsNativeScale(false); @@ -129,7 +141,7 @@ const PreviewZoomOverlay: React.FC = ({ scrollEl.scrollLeft = 0; scrollEl.scrollTop = 0; } - }, [open, activeDisplay?.url, displayKind]); + }, [open, previewEntry?.url, displayKind]); useEffect(() => { @@ -172,7 +184,7 @@ const PreviewZoomOverlay: React.FC = ({ }, [open]); useEffect(() => { - if (!activeDisplay?.url) { + if (!previewEntry?.url) { return; } @@ -182,10 +194,10 @@ const PreviewZoomOverlay: React.FC = ({ scrollEl.scrollTop = 0; } focusRef.current = null; - }, [activeDisplay?.url]); + }, [previewEntry?.url]); useEffect(() => { - if (!renderBackdrop || !activeDisplay?.url) { + if (!renderBackdrop || !previewEntry?.url) { return undefined; } @@ -194,7 +206,7 @@ const PreviewZoomOverlay: React.FC = ({ }); return () => cancelAnimationFrame(frame); - }, [renderBackdrop, activeDisplay?.url]); + }, [renderBackdrop, previewEntry?.url]); const handleKeyDown = (event: React.KeyboardEvent) => { event.stopPropagation(); @@ -229,20 +241,6 @@ const PreviewZoomOverlay: React.FC = ({ return; } - if (key === 'ArrowLeft') { - if (activeDisplay?.canGoPrev && activeDisplay?.goPrev) { - event.preventDefault(); - activeDisplay.goPrev(); - } - return; - } - - if (key === 'ArrowRight') { - if (activeDisplay?.canGoNext && activeDisplay?.goNext) { - event.preventDefault(); - activeDisplay.goNext(); - } - } }; const toggleZoomAtPoint = (clientX: number, clientY: number) => { @@ -273,7 +271,7 @@ const PreviewZoomOverlay: React.FC = ({ toggleZoomAtPoint(event.clientX ?? 0, event.clientY ?? 0); }; - const shouldRender = renderBackdrop && Boolean(activeDisplay?.url); + const shouldRender = renderBackdrop && Boolean(previewEntry?.url); const stageClassName = isPdfDisplay ? 'preview-zoom__stage preview-zoom__stage--pdf' @@ -314,8 +312,7 @@ const PreviewZoomOverlay: React.FC = ({ return null; } - const effectiveDisplay = activeDisplay; - const navVisible = Boolean(effectiveDisplay?.canGoPrev || effectiveDisplay?.canGoNext); + const effectiveDisplay = previewEntry; return createPortal( ( @@ -344,7 +341,7 @@ const PreviewZoomOverlay: React.FC = ({
@@ -352,7 +349,7 @@ const PreviewZoomOverlay: React.FC = ({ ) : ( {effectiveDisplay.alt { mediaRef.current = node; @@ -369,38 +366,6 @@ const PreviewZoomOverlay: React.FC = ({ /> )}
- {navVisible ? ( -
- - -
- ) : null} ), diff --git a/frontend/src/documents/panel/DocumentsPanel.tsx b/frontend/src/documents/panel/DocumentsPanel.tsx index ddddaee..6d462f2 100644 --- a/frontend/src/documents/panel/DocumentsPanel.tsx +++ b/frontend/src/documents/panel/DocumentsPanel.tsx @@ -162,6 +162,11 @@ const DocumentsPanel: React.FC = ({ const [previewZoomSource, setPreviewZoomSource] = useState(null); const zoomDisplay = previewZoomSource; + const overlayDocument = useMemo(() => ( + previewDoc && zoomDisplay?.url + ? { ...previewDoc, previewEntry: zoomDisplay } + : previewDoc + ), [previewDoc, zoomDisplay]); useEffect(() => { let cancelled = false; @@ -762,8 +767,8 @@ const DocumentsPanel: React.FC = ({ ); diff --git a/frontend/src/preview/DocumentViewerPanel.tsx b/frontend/src/preview/DocumentViewerPanel.tsx index 1fd9faa..0a1f88d 100644 --- a/frontend/src/preview/DocumentViewerPanel.tsx +++ b/frontend/src/preview/DocumentViewerPanel.tsx @@ -39,6 +39,11 @@ interface DocumentLike { version_number?: number; version?: { content_type?: string | null } | null; } | null; + previewEntry?: { + url: string; + alt?: string; + contentType?: string | null; + } | null; [key: string]: unknown; } @@ -53,10 +58,8 @@ interface DocumentViewerPanelProps extends DocumentSummarySectionProps { document: DocumentLike | null; previewEntry?: { url?: string; - canGoPrev?: boolean; - canGoNext?: boolean; - goPrev?: () => void; - goNext?: () => void; + contentType?: string | null; + filename?: string | null; } | null; hydrateDocument?: (doc: DocumentLike | null) => DocumentLike | null; ensureAssetUrl?: (docId: string | number, asset: AssetLike, options?: { start?: number; limit?: number }) => Promise; @@ -490,10 +493,16 @@ const DocumentViewerPanel: React.FC = ({ document?.title || 'Document preview' ); + const overlayDocument = useMemo(() => ( + document && zoomDisplay?.url + ? { ...document, previewEntry: zoomDisplay } + : document + ), [document, zoomDisplay]); + const overlay = ( ); diff --git a/frontend/src/preview/PdfViewer.tsx b/frontend/src/preview/PdfViewer.tsx index 3bb2332..424589b 100644 --- a/frontend/src/preview/PdfViewer.tsx +++ b/frontend/src/preview/PdfViewer.tsx @@ -90,7 +90,6 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX. pointerOffsetX: number; pointerOffsetY: number; } | null>(null); - const focusRatioRef = useRef<{ x: number; y: number } | null>(null); const ensureWasmUrl = useCallback(() => { if (!wasmUrlRef.current) { wasmUrlRef.current = resolvePdfWasmBaseUrl(); @@ -221,7 +220,6 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX. try { loadingTask = getDocument({ url: src, - withCredentials: true, wasmUrl, }); const pdf = await loadingTask.promise; diff --git a/frontend/src/styles/preview/preview-zoom.css b/frontend/src/styles/preview/preview-zoom.css index 72f902b..d32580b 100644 --- a/frontend/src/styles/preview/preview-zoom.css +++ b/frontend/src/styles/preview/preview-zoom.css @@ -74,60 +74,6 @@ align-items: flex-start; } -.preview-zoom__nav { - position: absolute; - bottom: 1em; - left: 50%; - transform: translateX(-50%); - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.9rem; - opacity: 0; - transition: opacity 0.18s ease; - pointer-events: none; -} - -.preview-zoom__stage:hover .preview-zoom__nav, -.preview-zoom__stage:focus-within .preview-zoom__nav { - opacity: 1; - pointer-events: auto; -} - -.preview-zoom__nav-button { - width: 2.8rem; - height: 2.8rem; - border: none; - border-radius: 999px; - background: var(--preview-nav-bg); - color: var(--preview-nav-fg); - display: inline-flex; - align-items: center; - justify-content: center; - cursor: pointer; - z-index: 1; - transition: background 0.15s ease, transform 0.15s ease; - box-shadow: 0 12px 28px var(--shadow-strong); -} - -.preview-zoom__nav-button:hover { - background: var(--preview-nav-bg-hover); -} - -.preview-zoom__nav-button:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 3px; -} - -.preview-zoom__nav-button svg { - width: 1.5rem; - height: 1.5rem; -} - -.preview-zoom__nav-button[disabled] { - opacity: 0.35; - cursor: default; -} .preview-zoom__pdf { width: 100%;