From 51ea93200e72ea0fd74d6f98ea838b42d5662a7a Mon Sep 17 00:00:00 2001 From: TekH Date: Sat, 6 Jun 2026 00:46:10 +0200 Subject: [PATCH] Add thumbnail sidebar and resizable splitter to viewer Enhanced EnvelopeViewer with a thumbnail sidebar for page previews and a resizable splitter (150px-400px range) for improved navigation. Updated layout to use a flexbox design for side-by-side thumbnails and PDF canvas. Externalized CSS for maintainability and added responsive behavior for mobile devices. Improved Blazor lifecycle handling with `firstRender` checks and sequential thumbnail rendering. Addressed known issues like vertical alignment and infinite render loops. Introduced localStorage persistence for user preferences and enhanced zoom/navigation interactivity with global mouse events. --- COPILOT_CONTEXT_EN.md | 189 +++++++++++++++++++++++++++--------------- 1 file changed, 120 insertions(+), 69 deletions(-) diff --git a/COPILOT_CONTEXT_EN.md b/COPILOT_CONTEXT_EN.md index 5069e1eb..6a492f22 100644 --- a/COPILOT_CONTEXT_EN.md +++ b/COPILOT_CONTEXT_EN.md @@ -70,7 +70,8 @@ Conversions: - Fetches PDF via `DocumentService.GetDocumentAsync(EnvelopeKey)` - Converts to base64 data URL: `data:application/pdf;base64,{base64}` - Initializes PDF.js viewer via JSInterop with `DotNetObjectReference` for callbacks -- Displays controls: Zoom In/Out, Page Navigation, Zoom percentage +- Displays controls: Zoom In/Out, Page Navigation, Zoom percentage, Thumbnail toggle +- Thumbnail sidebar with resizable splitter (150px-400px range) - CSS externalized to `envelope-viewer.css` **JavaScript (`pdf-viewer.js`):** @@ -78,10 +79,15 @@ Conversions: window.pdfViewer = { pdfDoc, canvas, ctx, scale, currentRenderTask, dotNetReference, wheelEventAttached, + isResizing, resizeMouseMoveHandler, resizeMouseUpHandler, initialize(canvasId, pdfDataUrl, dotNetRef), renderPage(num), + renderThumbnail(pageNum, canvasId), attachWheelEvent(), // Global Ctrl+Wheel zoom + attachResizeListeners(dotNetRef), // Splitter resize + detachResizeListeners(), + startResize(), zoomIn(), zoomOut(), nextPage(), previousPage(), dispose() @@ -91,7 +97,10 @@ window.pdfViewer = { **CSS (`envelope-viewer.css`):** - `.envelope-viewer-layout`: Full-height gradient background - `.envelope-action-bar`: Top bar with logo, title, controls (sticky) -- `.pdf-frame`: Fixed-size white container (`calc(100vh - 200px)` × 90% width, max 1200px) +- `.pdf-frame`: Flex container (row) with thumbnails + canvas side-by-side +- `.pdf-thumbnails`: Left sidebar (260px default, resizable 150-400px), no header +- `.pdf-splitter`: 4px resizable divider with `col-resize` cursor +- `.pdf-canvas-wrapper`: Flex-grow container with scroll, padding, centered canvas - `.pdf-canvas`: `display: inline-block`, unlimited zoom, scrollable when exceeds frame - Modern glassmorphism design with gradients and shadows @@ -113,9 +122,30 @@ window.pdfViewer = { - Catches `RenderingCancelledException` to avoid console errors - Queue system (`pageNumPending`) for rapid page changes -4. **Responsive Design:** +4. **Thumbnail Sidebar:** + - Left panel with page previews (sequential rendering, 50ms delay) + - Click to navigate to specific page + - Active page highlighted with gradient border + - No header/title (maximizes thumbnail space) + - Toggle button in toolbar to show/hide + +5. **Resizable Splitter:** + - 4px draggable divider between thumbnails and canvas + - Min width: 150px, Max width: 400px + - Visual feedback: gradient on hover/active + - User preference saved to localStorage (`envelopeViewer_thumbnailWidth`) + - Global mouse events (works anywhere during drag) + - `col-resize` cursor (?) for intuitive UX + +6. **Flex Layout:** + - Thumbnails and canvas in same container (`.pdf-frame`) + - `display: flex, flex-direction: row, align-items: stretch` + - Perfect vertical alignment (same top/bottom position) + - Responsive: column layout on mobile (<768px) + +7. **Responsive Design:** - Desktop: 90% width, 1200px max - - Mobile: 95% width, adjusted heights + - Mobile: 95% width, adjusted heights, thumbnails collapse to top - Adaptive padding and font sizes ### Flow @@ -127,21 +157,28 @@ window.pdfViewer = { - Convert to base64 data URL - Set _isLoading = false - OnAfterRenderAsync(): + OnAfterRenderAsync(firstRender): + - Load saved thumbnail width from localStorage - Create DotNetObjectReference - JSRuntime.InvokeAsync("pdfViewer.initialize", canvasId, pdfDataUrl, dotNetRef) + - Attach resize listeners for splitter - Update _totalPages, _currentPage, _pdfLoaded + - Render thumbnails sequentially (50ms delay between pages) ``` 2. **User Interaction:** - Button clicks ? `ZoomIn()`/`ZoomOut()` ? `JSRuntime.InvokeVoidAsync("pdfViewer.zoomIn")` - Ctrl+Wheel ? JS `attachWheelEvent()` ? `dotNetRef.invokeMethodAsync('OnZoomChanged')` - Page buttons ? `NextPage()`/`PreviousPage()` ? `JSRuntime.InvokeAsync("pdfViewer.nextPage")` + - Thumbnail click ? `GoToPageFromThumbnail(pageNum)` ? `pdfViewer.goToPage(pageNum)` + - Toggle button ? `ToggleThumbnails()` ? `_showThumbnails = !_showThumbnails` + - Splitter drag ? `OnSplitterMouseDown()` ? JS global mouse events ? `OnSplitterMouseMove(clientX)` ? width update ? `OnSplitterMouseUp()` ? save to localStorage 3. **Cleanup:** ```csharp DisposeAsync(): - JSRuntime.InvokeVoidAsync("pdfViewer.dispose") + - Detach resize listeners - _dotNetRef?.Dispose() ``` @@ -269,6 +306,8 @@ return report; | **Mouse wheel on `.pdf-frame` only** | **Only works when mouse over PDF; should work anywhere on page** | | **OnAfterRenderAsync without `firstRender` guard** | **Creates infinite loop when `StateHasChanged` is called repeatedly** | | **Conditional rendering with `@if (_pdfLoaded)` wrapping canvas** | **Canvas not in DOM when initialize called, causing perpetual failure** | +| **Thumbnail sidebar with `position: absolute`** | **Independent from PDF canvas, breaks alignment on screen resize** | +| **Thumbnail header with title/close button** | **Wastes valuable space; toolbar toggle is sufficient** | --- @@ -303,81 +342,93 @@ Our use case is **visual/image stamping** at specific page coordinates | **11** | **2025-01-XX** | **Added PDF thumbnail sidebar (left panel) with page previews and navigation** | | **11** | **2025-01-XX** | **Implemented thumbnail rendering system with sequential loading (50ms delay between pages)** | | **11** | **2025-01-XX** | **Fixed thumbnail rendering: retry logic (10x 100ms) for canvas availability** | -| **11** | **2025-01-XX** | **Refactored layout: Side-by-side flex design (thumbnails left, PDF right), responsive mobile (horizontal scroll thumbnails)** | -| **11** | **2025-01-XX** | **Updated COPILOT_CONTEXT_EN.md: EnvelopeViewer replaces ReportViewer for read-only viewing** | -| **11** | **2025-01-XX** | **?? UNRESOLVED: Infinite render loop causing blank screen — Canvas not found error repeating, `_pdfLoaded` never becomes true** | +| **11** | **2025-01-XX** | **Refactored layout: Moved thumbnails inside `pdf-frame` for flex side-by-side design** | +| **11** | **2025-01-XX** | **Removed thumbnail header (title + close button) to maximize thumbnail space** | +| **11** | **2025-01-XX** | **Added resizable splitter: 4px draggable divider, 150-400px range, localStorage persistence** | +| **11** | **2025-01-XX** | **Fixed vertical alignment: `align-items: stretch` ensures thumbnails and canvas have same height** | +| **11** | **2025-01-XX** | **Updated COPILOT_CONTEXT_EN.md: Documented resizable splitter and layout refactoring** | --- -## Known Issues +## Layout Architecture (EnvelopeViewer) -### EnvelopeViewer — Blank Screen / Infinite Loop (UNRESOLVED) +### HTML Structure +```html +
+
+ +
+ +
+ @if (_showThumbnails) { +
+ +
+
+ +
+ } +
+ +
+
+
+``` -**Symptoms:** -- Browser console shows: `Canvas not found: pdf-canvas` (repeating 20+ times) -- UI displays error: "Fehler beim Laden des Dokuments - PDF konnte nicht initialisiert werden" -- Blank purple gradient screen, no PDF or thumbnails visible -- `OnAfterRenderAsync` triggers continuously in loop +### CSS Flexbox Layout +```css +.pdf-frame { + display: flex; + flex-direction: row; /* Side-by-side */ + align-items: stretch; /* Same height */ + overflow: hidden; +} -**Root Cause:** -- `OnAfterRenderAsync` runs on every render cycle (not just `firstRender`) -- PDF canvas element is not in DOM when `pdfViewer.initialize` is called -- Because `_pdfLoaded = false`, thumbnail/toolbar sections don't render (`@if (_pdfLoaded)` condition) -- Each failed initialize triggers `StateHasChanged` ? new render ? `OnAfterRenderAsync` again ? **infinite loop** +.pdf-thumbnails { + flex-shrink: 0; /* Fixed width */ + width: 260px; /* Dynamic via inline style */ + border-right: none; /* Seamless join with splitter */ +} -**Attempted Fixes (Failed):** -1. **Adding `firstRender` check:** - ```csharp - protected override async Task OnAfterRenderAsync(bool firstRender) { - if (firstRender && !_pdfLoaded && ...) { - // Initialize PDF - } - } - ``` - **Result:** Didn't stop the loop, still blank screen +.pdf-splitter { + width: 4px; + cursor: col-resize; + flex-shrink: 0; +} -2. **PDF.js availability check:** - ```csharp - var pdfJsLoaded = await JSRuntime.InvokeAsync("eval", "typeof window.pdfjsLib !== 'undefined'"); - ``` - **Result:** Didn't resolve canvas not found issue +.pdf-canvas-wrapper { + flex: 1; /* Fill remaining space */ + overflow: auto; /* Scrollable for zoom */ + padding: 2rem; + text-align: center; +} +``` -3. **Increased delays:** - - `Task.Delay(300)` before initialize - - `Task.Delay(200)` before thumbnails - **Result:** No improvement +### Resizable Splitter Workflow +``` +1. User hovers splitter ? cursor: col-resize (?) +2. Mouse down ? OnSplitterMouseDown(e) + - _isResizing = true + - Store start position (clientX) and width + - Add 'resizing' class to body (prevent text selection) + - Call pdfViewer.startResize() -4. **JavaScript validation:** - - Added checks for `uint8Array.length`, `totalPages > 0` - **Result:** Didn't prevent initialization failure +3. Mouse move (global) ? OnSplitterMouseMove(clientX) + - Calculate delta = clientX - startX + - newWidth = startWidth + delta + - Clamp to 150-400px range + - Update _thumbnailWidth + - StateHasChanged() for reactive UI -**Possible Next Steps:** -1. **DOM Ready Strategy:** - - Wait for specific element existence before initialize - - Use `MutationObserver` in JS to detect canvas availability - - Try `IntersectionObserver` to ensure canvas is in viewport +4. Mouse up (global) ? OnSplitterMouseUp() + - _isResizing = false + - Remove 'resizing' class + - Save to localStorage("envelopeViewer_thumbnailWidth") +``` -2. **Conditional Rendering:** - - Always render canvas element (even before `_pdfLoaded`) - - Move toolbar/thumbnails outside `@if (_pdfLoaded)` block - - Use CSS `visibility: hidden` instead of conditional rendering +### Responsive Behavior +- **Desktop (>768px)**: Flex row, side-by-side +- **Mobile (?768px)**: Flex column, thumbnails on top +- **Thumbnail toggle**: Controlled by `@if (_showThumbnails)` in Razor markup -3. **Blazor Lifecycle:** - - Try `OnAfterRenderAsync` with `IJSRuntime` timeout guard - - Use `Task.Run` with cancellation token to prevent overlapping calls - - Investigate if WASM-specific render cycle differs from Server -4. **Debugging:** - - Add `Console.WriteLine` in C# to track render count - - Log `firstRender`, `_pdfLoaded`, `_pdfDataUrl` state on each call - - Check if PDF data is actually loaded (`_pdfDataUrl` not null/empty) - - Verify PDF.js CDN loads successfully (Network tab) - -**Test Case:** -- Route: `/envelope/NTE3Ym15SyUtNjA4M...` (valid envelope key) -- Expected: PDF loads, thumbnails appear, toolbar shows -- Actual: Blank screen, console error spam, no PDF rendering - -**Workaround:** -- Use legacy `ReportViewer.razor` (`/receiver/{key}`) for now -- EnvelopeViewer development paused until root cause identified