diff --git a/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor b/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor
index 8253d971..9efca85d 100644
--- a/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor
+++ b/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeViewer.razor
@@ -130,7 +130,7 @@
@if (_pdfLoaded && _showThumbnails) {
-
+
@for (int i = 1; i <= _totalPages; i++) {
var pageNum = i;
@@ -143,6 +143,11 @@
}
+
+
+
}
@@ -165,19 +170,27 @@
@code {
- [Parameter] public string? EnvelopeKey { get; set; }
+[Parameter] public string? EnvelopeKey { get; set; }
- bool _isLoading = true;
- string? _errorMessage;
- string? _pdfDataUrl;
- bool _pdfLoaded = false;
- int _currentPage = 1;
- int _totalPages = 0;
- int _currentZoom = 150;
- bool _showThumbnails = true;
- DotNetObjectReference
? _dotNetRef;
+bool _isLoading = true;
+string? _errorMessage;
+string? _pdfDataUrl;
+bool _pdfLoaded = false;
+int _currentPage = 1;
+int _totalPages = 0;
+int _currentZoom = 150;
+bool _showThumbnails = true;
+DotNetObjectReference? _dotNetRef;
- protected override async Task OnInitializedAsync() {
+// Resizable splitter state
+int _thumbnailWidth = 260;
+bool _isResizing = false;
+int _resizeStartX = 0;
+int _resizeStartWidth = 0;
+const int MinThumbnailWidth = 150;
+const int MaxThumbnailWidth = 400;
+
+protected override async Task OnInitializedAsync() {
if (string.IsNullOrWhiteSpace(EnvelopeKey)) {
_errorMessage = "Envelope-Schlüssel fehlt.";
_isLoading = false;
@@ -205,6 +218,19 @@
}
protected override async Task OnAfterRenderAsync(bool firstRender) {
+ if (firstRender) {
+ // Load saved thumbnail width from localStorage
+ try {
+ var savedWidth = await JSRuntime.InvokeAsync("localStorage.getItem", "envelopeViewer_thumbnailWidth");
+ if (!string.IsNullOrEmpty(savedWidth) && int.TryParse(savedWidth, out var width)) {
+ _thumbnailWidth = Math.Clamp(width, MinThumbnailWidth, MaxThumbnailWidth);
+ await InvokeAsync(StateHasChanged);
+ }
+ } catch {
+ // Ignore localStorage errors
+ }
+ }
+
if (!_pdfLoaded && !string.IsNullOrWhiteSpace(_pdfDataUrl)) {
await Task.Delay(500);
@@ -216,6 +242,10 @@
_pdfLoaded = true;
_totalPages = await JSRuntime.InvokeAsync("pdfViewer.getTotalPages");
_currentPage = await JSRuntime.InvokeAsync("pdfViewer.getCurrentPage");
+
+ // Attach resize listeners
+ await JSRuntime.InvokeVoidAsync("pdfViewer.attachResizeListeners", _dotNetRef);
+
await InvokeAsync(StateHasChanged);
// Wait for DOM to be ready, then render thumbnails
@@ -315,6 +345,45 @@
}
}
+ // Resizable splitter methods
+ void OnSplitterMouseDown(MouseEventArgs e) {
+ _isResizing = true;
+ _resizeStartX = (int)e.ClientX;
+ _resizeStartWidth = _thumbnailWidth;
+
+ // Add resizing class to body to prevent text selection
+ _ = JSRuntime.InvokeVoidAsync("eval", "document.body.classList.add('resizing')");
+ _ = JSRuntime.InvokeVoidAsync("pdfViewer.startResize");
+ }
+
+ [JSInvokable]
+ public async Task OnSplitterMouseMove(int clientX) {
+ if (!_isResizing) return;
+
+ var delta = clientX - _resizeStartX;
+ var newWidth = _resizeStartWidth + delta;
+
+ // Clamp to min/max
+ _thumbnailWidth = Math.Clamp(newWidth, MinThumbnailWidth, MaxThumbnailWidth);
+
+ await InvokeAsync(StateHasChanged);
+ }
+
+ [JSInvokable]
+ public async Task OnSplitterMouseUp() {
+ if (!_isResizing) return;
+
+ _isResizing = false;
+
+ // Remove resizing class from body
+ await JSRuntime.InvokeVoidAsync("eval", "document.body.classList.remove('resizing')");
+
+ // Save preference to localStorage
+ await JSRuntime.InvokeVoidAsync("localStorage.setItem", "envelopeViewer_thumbnailWidth", _thumbnailWidth.ToString());
+
+ await InvokeAsync(StateHasChanged);
+ }
+
public async ValueTask DisposeAsync() {
if (_pdfLoaded) {
try {
diff --git a/EnvelopeGenerator.ReceiverUI/wwwroot/css/envelope-viewer.css b/EnvelopeGenerator.ReceiverUI/wwwroot/css/envelope-viewer.css
index 1b532cfb..82179019 100644
--- a/EnvelopeGenerator.ReceiverUI/wwwroot/css/envelope-viewer.css
+++ b/EnvelopeGenerator.ReceiverUI/wwwroot/css/envelope-viewer.css
@@ -104,6 +104,46 @@
background: linear-gradient(135deg, #6b1cb0 0%, #1e3a72 100%);
}
+.pdf-splitter {
+ width: 4px;
+ background: transparent;
+ cursor: col-resize;
+ flex-shrink: 0;
+ position: relative;
+ transition: background 0.2s ease;
+ z-index: 10;
+ user-select: none;
+}
+
+.pdf-splitter::before {
+ content: '';
+ position: absolute;
+ left: -4px;
+ right: -4px;
+ top: 0;
+ bottom: 0;
+ /* Enlarged hitbox for easier grabbing */
+}
+
+.pdf-splitter:hover,
+.pdf-splitter.resizing {
+ background: linear-gradient(90deg,
+ rgba(126, 34, 206, 0.4) 0%,
+ rgba(42, 82, 152, 0.4) 100%);
+}
+
+.pdf-splitter:active {
+ background: linear-gradient(90deg,
+ rgba(126, 34, 206, 0.6) 0%,
+ rgba(42, 82, 152, 0.6) 100%);
+}
+
+/* Prevent text selection during resize */
+body.resizing {
+ user-select: none;
+ cursor: col-resize !important;
+}
+
.pdf-thumbnail {
cursor: pointer;
border-radius: 8px;
diff --git a/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js b/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js
index 91c8f650..815a66e2 100644
--- a/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js
+++ b/EnvelopeGenerator.ReceiverUI/wwwroot/js/pdf-viewer.js
@@ -289,6 +289,48 @@ window.pdfViewer = {
this.wheelEventAttached = false;
this.dotNetReference = null;
}
+ this.detachResizeListeners();
+ },
+
+ // Resizable splitter functionality
+ isResizing: false,
+ resizeMouseMoveHandler: null,
+ resizeMouseUpHandler: null,
+
+ attachResizeListeners(dotNetRef) {
+ this.dotNetReference = dotNetRef;
+
+ this.resizeMouseMoveHandler = (e) => {
+ if (this.isResizing && this.dotNetReference) {
+ this.dotNetReference.invokeMethodAsync('OnSplitterMouseMove', e.clientX);
+ }
+ };
+
+ this.resizeMouseUpHandler = () => {
+ if (this.isResizing && this.dotNetReference) {
+ this.isResizing = false;
+ this.dotNetReference.invokeMethodAsync('OnSplitterMouseUp');
+ }
+ };
+
+ document.addEventListener('mousemove', this.resizeMouseMoveHandler);
+ document.addEventListener('mouseup', this.resizeMouseUpHandler);
+ },
+
+ detachResizeListeners() {
+ if (this.resizeMouseMoveHandler) {
+ document.removeEventListener('mousemove', this.resizeMouseMoveHandler);
+ this.resizeMouseMoveHandler = null;
+ }
+ if (this.resizeMouseUpHandler) {
+ document.removeEventListener('mouseup', this.resizeMouseUpHandler);
+ this.resizeMouseUpHandler = null;
+ }
+ this.isResizing = false;
+ },
+
+ startResize() {
+ this.isResizing = true;
}
};