Enhanced the "Mistakes History" table in `COPILOT_CONTEXT_EN.md`: - Added a "Session" column to track when mistakes occurred. - Updated table with session numbers for existing mistakes. - Added new entries documenting recurring issues like over-engineering, ignoring revert instructions, and user feedback. - Highlighted the importance of configurability and simplicity in design. - Documented specific mistakes related to DevExpress and PDF.js. These changes improve traceability, accountability, and alignment with user preferences.
469 lines
18 KiB
Markdown
469 lines
18 KiB
Markdown
# EnvelopeGenerator — Copilot Context Notes (English)
|
||
|
||
## Purpose
|
||
A digital document signing system. Senders upload PDFs and place signature annotation fields via PSPDFKit (EnvelopeGenerator.Web). Receivers open the document in a Blazor WASM viewer, confirm each signature field via a checkbox overlay, draw/type/upload their signature, and export the stamped PDF.
|
||
|
||
---
|
||
|
||
## Solution Structure
|
||
|
||
| Project | Target | Description |
|
||
|---|---|---|
|
||
| `EnvelopeGenerator.API` | net8.0 | ASP.NET Core Web API. Receiver auth (cookie), annotation reading, PDF serving. |
|
||
| `EnvelopeGenerator.ReceiverUI` | net8.0 WASM | Blazor WebAssembly. Receiver UI. YARP proxies API calls. |
|
||
| `EnvelopeGenerator.Web` | net7/8/9 | Razor Pages. Sender UI + PSPDFKit annotation placement. |
|
||
| `EnvelopeGenerator.Application` | multi | MediatR CQRS handlers. |
|
||
| `EnvelopeGenerator.Domain` | multi | Domain models, constants, interfaces. |
|
||
| `EnvelopeGenerator.Infrastructure` | multi | EF Core repos, DB context. |
|
||
| `EnvelopeGenerator.PdfEditor` | multi | iText7 utilities. NOT used in ReceiverUI flow. |
|
||
| `EnvelopeGenerator.DependencyInjection` | multi | DI registration helpers. |
|
||
| VB.NET projects (Service/Form/BBTests) | net462 | Legacy. Do NOT touch. |
|
||
|
||
---
|
||
|
||
## Key Files
|
||
|
||
| File | Purpose |
|
||
|---|---|
|
||
| `ReceiverUI/Pages/EnvelopeViewer.razor` | **NEW** PDF.js-based viewer (`/envelope/{key}`). Replaces ReportViewer.razor. Simple read-only PDF viewing with zoom/navigation. |
|
||
| `ReceiverUI/Pages/ReportViewer.razor` | **LEGACY** DevExpress-based signing page (`/receiver/{key}`). Still used for signature workflow. Will be deprecated. |
|
||
| `ReceiverUI/wwwroot/js/pdf-viewer.js` | **NEW** PDF.js wrapper: rendering, zoom, pagination, mouse wheel control. |
|
||
| `ReceiverUI/wwwroot/js/receiver-signature.js` | JS: checkbox overlay, signature pad (draw/type/image). |
|
||
| `ReceiverUI/wwwroot/css/envelope-viewer.css` | **NEW** Styles for EnvelopeViewer.razor (external CSS, not inline). |
|
||
| `ReceiverUI/wwwroot/fake-data/annotations.json` | Dev-mode fake annotations (YARP proxy target). |
|
||
| `ReceiverUI/Models/AnnotationDto.cs` | Annotation position model. All properties non-nullable. |
|
||
| `ReceiverUI/Services/AnnotationService.cs` | Fetches `List<AnnotationDto>` from API or fake-data. |
|
||
| `ReceiverUI/Services/DocumentService.cs` | Fetches PDF bytes from API. |
|
||
| `ReceiverUI/Services/AuthService.cs` | Manages receiver session cookie. |
|
||
| `API/Controllers/AnnotationController.cs` | GET `api/Annotation/{key}` ? annotation list. |
|
||
| `API/Controllers/DocumentController.cs` | GET `api/Document/{key}` ? PDF bytes. |
|
||
|
||
---
|
||
|
||
## AnnotationDto — Coordinate System
|
||
|
||
```
|
||
Unit : 1/100 inch (DX units) — DevExpress XtraReports native
|
||
Origin : Top-left corner of page
|
||
X : increases rightward
|
||
Y : increases downward
|
||
|
||
A4 in DX units: Width = 827, Height = 1169
|
||
|
||
Conversions:
|
||
PSPDFKit (pt, top-left): xDX = xPsPdf * (100/72)
|
||
GDPicture (pt, bottom-left): yDX = (pageHeightPt - yGD - elemHeightPt) * (100/72)
|
||
DX ? PDF points: pt = dx * (72/100)
|
||
```
|
||
|
||
---
|
||
|
||
## EnvelopeViewer (NEW) — PDF.js Read-Only Viewer
|
||
|
||
**Route:** `/envelope/{EnvelopeKey}`
|
||
**Purpose:** Modern, high-performance PDF viewing without signing functionality.
|
||
**Technology:** PDF.js 3.11.174 + custom JavaScript + configurable quality settings
|
||
|
||
### Architecture
|
||
|
||
**Blazor Component (`EnvelopeViewer.razor`):**
|
||
- 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
|
||
- Quality settings loaded from `appsettings.json` via `IOptions<PdfViewerOptions>`
|
||
- Thumbnail sidebar with resizable splitter (150px-400px, localStorage persistence)
|
||
- CSS externalized to `envelope-viewer.css`
|
||
|
||
**JavaScript (`pdf-viewer.js`):**
|
||
```javascript
|
||
window.pdfViewer = {
|
||
qualityOptions, // Configurable from appsettings.json
|
||
setQualityOptions(options), // Dynamic quality update
|
||
initialize(canvasId, pdfDataUrl, dotNetRef),
|
||
renderPage(num),
|
||
renderThumbnail(pageNum, canvasId),
|
||
attachWheelEvent(), // Ctrl+Wheel zoom (configurable step)
|
||
zoomIn(), zoomOut(), // Configurable step percentage
|
||
dispose()
|
||
}
|
||
```
|
||
|
||
**Options (`PdfViewerOptions.cs`):**
|
||
```csharp
|
||
public class PdfViewerOptions {
|
||
public double ThumbnailBaseScale { get; set; } = 0.75; // 0.2-1.5
|
||
public bool ThumbnailEnableHiDPI { get; set; } = true;
|
||
public double ThumbnailMaxDPR { get; set; } = 2.0; // 1.0-3.0
|
||
public bool MainCanvasEnableHiDPI { get; set; } = true;
|
||
public double MainCanvasMaxDPR { get; set; } = 2.0;
|
||
public bool EnableSmoothZoom { get; set; } = true;
|
||
public int ZoomTransitionDuration { get; set; } = 150; // ms
|
||
public double RenderingOpacity { get; set; } = 0.85; // 0.0-1.0
|
||
public int ThumbnailRenderDelay { get; set; } = 50; // ms
|
||
public int ZoomStepPercentage { get; set; } = 5; // 1-50%
|
||
}
|
||
```
|
||
|
||
### Configuration (appsettings.json)
|
||
|
||
**Location:** `EnvelopeGenerator.ReceiverUI/wwwroot/appsettings.json`
|
||
|
||
```json
|
||
{
|
||
"PdfViewer": {
|
||
"ThumbnailBaseScale": 0.75,
|
||
"ThumbnailEnableHiDPI": true,
|
||
"ThumbnailMaxDPR": 2.0,
|
||
"MainCanvasEnableHiDPI": true,
|
||
"MainCanvasMaxDPR": 2.0,
|
||
"EnableSmoothZoom": true,
|
||
"ZoomTransitionDuration": 150,
|
||
"RenderingOpacity": 0.85,
|
||
"ThumbnailRenderDelay": 50,
|
||
"ZoomStepPercentage": 5
|
||
}
|
||
}
|
||
```
|
||
|
||
**Usage:** Edit file ? F5 (browser refresh) ? new settings applied
|
||
|
||
**Presets:**
|
||
- **High Quality**: `ThumbnailBaseScale: 1.0, MaxDPR: 3.0` (powerful devices)
|
||
- **Balanced**: Default values (recommended)
|
||
- **Performance**: `ThumbnailBaseScale: 0.5, EnableHiDPI: false` (mobile/low-end)
|
||
|
||
---
|
||
|
||
### Features
|
||
|
||
1. **HiDPI/Retina Support** ? 4x quality on Retina displays
|
||
2. **Configurable Quality** ? All parameters in appsettings.json
|
||
3. **Unlimited Zoom** ? 50%-300%, configurable step (default 5%)
|
||
4. **Global Ctrl+Wheel Zoom** ? Works anywhere on page
|
||
5. **Thumbnail Sidebar** ? Resizable (150-400px), high-quality rendering
|
||
6. **Smooth Transitions** ? Configurable fade effect
|
||
7. **Responsive Design** ? Desktop/mobile adaptive layout
|
||
|
||
---
|
||
|
||
### Features
|
||
|
||
1. **Unlimited Zoom:**
|
||
- Canvas size not restricted by `max-width`
|
||
- Frame stays fixed, scroll bars appear automatically
|
||
- `text-align: center` for small sizes, full scroll for zoomed views
|
||
|
||
2. **Global Mouse Wheel Zoom:**
|
||
- Event listener on `document.body` (works anywhere on page)
|
||
- `Ctrl + Mouse Wheel` triggers `zoomIn()`/`zoomOut()`
|
||
- Calls `dotNetReference.invokeMethodAsync('OnZoomChanged', scale)` to update Blazor UI
|
||
- `{ passive: false }` to enable `preventDefault()`
|
||
|
||
3. **Render Task Cancellation:**
|
||
- Stores `currentRenderTask` to cancel previous render if new one starts
|
||
- Catches `RenderingCancelledException` to avoid console errors
|
||
- Queue system (`pageNumPending`) for rapid page changes
|
||
|
||
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, thumbnails collapse to top
|
||
- Adaptive padding and font sizes
|
||
|
||
### Initialization Flow
|
||
|
||
```csharp
|
||
OnInitializedAsync():
|
||
1. Fetch PDF bytes from DocumentService
|
||
2. Convert to base64 data URL
|
||
3. Set _isLoading = false
|
||
|
||
OnAfterRenderAsync(firstRender):
|
||
1. Load saved thumbnail width from localStorage
|
||
2. Create DotNetObjectReference
|
||
3. Send PdfViewerOptions to JavaScript
|
||
4. Initialize PDF.js viewer
|
||
5. Attach splitter resize listeners
|
||
6. Render thumbnails sequentially (configurable delay)
|
||
```
|
||
|
||
**User Interactions:**
|
||
- Zoom: Buttons/Ctrl+Wheel/Slider ? configurable step percentage
|
||
- Pages: Buttons/Input/Thumbnails ? navigate
|
||
- Sidebar: Toggle button ? show/hide thumbnails
|
||
- Splitter: Drag ? resize sidebar (150-400px)
|
||
|
||
**Cleanup:**
|
||
```csharp
|
||
DisposeAsync():
|
||
- Dispose PDF.js viewer
|
||
- Detach event listeners
|
||
- Dispose DotNetObjectReference
|
||
```
|
||
|
||
---
|
||
|
||
### Key Differences from ReportViewer
|
||
|
||
| Feature | EnvelopeViewer (NEW) | ReportViewer (LEGACY) |
|
||
|---------|----------------------|------------------------|
|
||
| Technology | PDF.js + Canvas | DevExpress XtraReports |
|
||
| Route | `/envelope/{key}` | `/receiver/{key}` |
|
||
| Purpose | Read-only viewing | Signature workflow |
|
||
| Dependencies | PDF.js CDN | DevExpress NuGet packages |
|
||
| Zoom | Unlimited, smooth | Report viewer default |
|
||
| Mouse Wheel | Custom Ctrl+Wheel | Browser default |
|
||
| File Size | Minimal (JS + CSS) | Heavy (DX libs) |
|
||
| Maintenance | Simple, standard web | Complex, vendor-specific |
|
||
|
||
---
|
||
|
||
## ReceiverUI Signing Flow (ReportViewer.razor — LEGACY)
|
||
|
||
### On Load (`OnInitializedAsync`)
|
||
1. `AuthService.CheckEnvelopeAccessAsync` ? redirect to login if unauthorized
|
||
2. `AnnotationService.GetAnnotationsAsync` ? fills `_annotations`
|
||
3. `DocumentService.GetDocumentAsync` ? fills `_basePdfBytes` (real mode)
|
||
4. `BuildFreshBaseReport()` ? `XtraReport` for `DxReportViewer`
|
||
|
||
### Signature Popup
|
||
- Tabs: Draw / Text / Image
|
||
- Fields: full name (required), position (optional), place (required)
|
||
- Saved to `_capturedSignature` record
|
||
- If annotations exist ? popup closes ? JS checkbox overlays installed
|
||
|
||
### JS Checkbox Overlay (`receiver-signature.js`)
|
||
- `receiverSignature.installAnnotationCheckboxes(annotations, checkedIds, dotNetRef)`
|
||
- One `.annot-sig-cb-wrapper` div per annotation, absolutely positioned over viewer scroll container
|
||
- Position: `left = pageRect.left + ann.x * scaleX`, `top = pageRect.top + ann.y * scaleY`
|
||
- `scaleX = pagePixelWidth / 827`, `scaleY = pagePixelHeight / 1169`
|
||
- Click ? `dotNetRef.invokeMethodAsync('OnAnnotationToggled', id, checked)`
|
||
|
||
### Apply Signatures ("Unterschriften anwenden" — `SubmitSignaturesAsync`)
|
||
|
||
**Real PDF mode (`_basePdfBytes` is set):**
|
||
- Calls `StampSignaturesOnPdf` using **iText7** directly on PDF bytes
|
||
- Coordinate conversion: `xPt = ann.X * (72/100)`, `yPt = pageHeight - ann.Y * (72/100) - sigHeight` (Y flip: DX top-down ? PDF bottom-up)
|
||
- Returns stamped bytes ? loaded into new `XtraReport` with `XRPdfContent`
|
||
- Viewer refreshed with `ViewerKey++`
|
||
|
||
**Dev/fake mode (`_basePdfBytes` is null):**
|
||
- Falls back to `AddSignatureAtAnnotation` (XtraReports DetailBand + BeforePrint counter)
|
||
- This is intentionally left as a dev fallback only
|
||
|
||
### Export
|
||
- `reportViewer.ExportToAsync(ExportFormat.Pdf)`
|
||
|
||
---
|
||
|
||
## StampSignaturesOnPdf — iText7 Implementation
|
||
|
||
```csharp
|
||
// Located in ReportViewer.razor @code section
|
||
// Called by SubmitSignaturesAsync when _basePdfBytes is available
|
||
|
||
static byte[] StampSignaturesOnPdf(
|
||
byte[] sourcePdfBytes, byte[] signatureImageBytes,
|
||
string signerFullName, string signerPosition, string signaturePlace,
|
||
IReadOnlyList<AnnotationDto> annotations)
|
||
{
|
||
// Opens PDF with PdfReader/PdfWriter
|
||
// For each annotation:
|
||
// pageNum = ann.Page (clamped to totalPages)
|
||
// xPt = ann.X * (72f/100f)
|
||
// imgBottomY = pageHeight - ann.Y * (72f/100f) - sigHeightPt ? Y-axis flip
|
||
// PdfCanvas.AddImageFittedIntoRectangle(imageData, rect, false)
|
||
// Separator line at bottom of image
|
||
// Canvas text block below separator (font: Helvetica 7pt, color: RGB(73,80,87))
|
||
// Returns stamped byte[]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## BuildFreshBaseReport()
|
||
|
||
```csharp
|
||
// Real PDF mode:
|
||
var report = new XtraReport();
|
||
var detail = new DetailBand();
|
||
report.Bands.Add(detail);
|
||
detail.Controls.Add(new XRPdfContent { Source = _basePdfBytes, GenerateOwnPages = true });
|
||
return report;
|
||
|
||
// Dev/fake mode: returns pre-built report from ReportStorage
|
||
```
|
||
|
||
---
|
||
|
||
## NuGet Packages in ReceiverUI
|
||
|
||
| Package | Version | Purpose |
|
||
|---|---|---|
|
||
| `DevExpress.Blazor.Reporting.Viewer` | 25.2.3 | DxReportViewer (LEGACY, used in ReportViewer.razor) |
|
||
| `DevExpress.Blazor.PdfViewer` | 25.2.3 | PDF viewer (not used in EnvelopeViewer) |
|
||
| `DevExpress.Drawing.Skia` | 25.2.3 | Drawing backend |
|
||
| `itext` | 8.0.5 | PDF stamping (iText7) |
|
||
| `SkiaSharp.*` | 3.119.1 | WASM native rendering |
|
||
|
||
**External CDN (EnvelopeViewer):**
|
||
- PDF.js 3.11.174 (via `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`)
|
||
- PDF.js Worker (`pdf.worker.min.js`)
|
||
|
||
---
|
||
|
||
## Mistakes History — Do NOT Repeat
|
||
|
||
| Mistake | Why Wrong | Session |
|
||
|---|---|---|
|
||
| `BottomMarginBand` for per-page signatures | Repeats on every page; Y offset wrong | 4 |
|
||
| `imageY = (page-1) * 1169 + ann.Y` | Inflates DetailBand; 35 pages ? 140 pages | 8 |
|
||
| `e.Graph?.PrintingSystem` in BeforePrint | `Graph` not on `CancelEventArgs` | 5 |
|
||
| `ctrl.Report?.PrintingSystem` | `PrintingSystem` not on `XtraReportBase` in WASM | — |
|
||
| Adding stamp endpoint to `DocumentController` | Not needed; stamping is done client-side in ReceiverUI | — |
|
||
| iText7 via API (server-side) | Unnecessary; iText7 runs fine in WASM directly | 10 |
|
||
| **PDF.js: Hardcoded quality values** | **Use appsettings.json for configurability** | **11** |
|
||
| **PDF.js: Hardcoded zoom step (1%)** | **Too granular; use configurable percentage** | **11** |
|
||
| **Toolbar: Complex left/center/right layout** | **User wants simple horizontal layout; failed multiple times to implement** | **11** |
|
||
| **Zoom label: Badge style (gradient/border/padding)** | **Over-designed; user prefers simple text label** | **11** |
|
||
| **Attempting to "improve" simple designs** | **User requests simplicity; AI keeps over-engineering** | **11** |
|
||
| **Ignoring explicit "revert" instructions** | **User said revert toolbar, AI tried to fix CSS instead of reverting HTML structure** | **11** |
|
||
|
||
---
|
||
|
||
## DevExpress Article (2023-08-28) — Why It Does NOT Apply
|
||
|
||
The article describes **X.509 cryptographic digital signatures** via `PdfDocumentSigner` + `Pkcs7Signer`.
|
||
Our use case is **visual/image stamping** at specific page coordinates — different problem, different API.
|
||
`XRPdfSignature` in the article requires pre-placed fields in the report designer, not runtime coordinates.
|
||
|
||
---
|
||
|
||
## Change Log
|
||
|
||
| Session | Date | Change |
|
||
|---|---|---|
|
||
| 1–3 | — | Core infrastructure: services, YARP proxy, JS overlay, signature pad |
|
||
| 4 | — | `AddSignatureAtAnnotation` with BottomMarginBand — ? repeated on all pages |
|
||
| 5 | — | `BeforePrint` + `e.Graph?.PrintingSystem` — ? compile error |
|
||
| 6 | — | BeforePrint counter — ? correct pattern, wrong band |
|
||
| 7 | — | Switched to DetailBand — ? correct band |
|
||
| 8 | — | `(page-1)*1169+Y` offset — ? 35?140 page inflation |
|
||
| 9 | — | Fixed: `BoundsF.Y = ann.Y` + counter; created COPILOT_CONTEXT.md |
|
||
| 10 | — | Investigated DevExpress article — not applicable to our case |
|
||
| 10 | — | Added iText7 to ReceiverUI; implemented `StampSignaturesOnPdf` — ? deterministic coordinates, no page count side effects |
|
||
| 10 | — | Split COPILOT_CONTEXT.md into COPILOT_CONTEXT_EN.md and COPILOT_CONTEXT_TR.md |
|
||
| **11** | **2025-01-XX** | **Created EnvelopeViewer.razor with PDF.js 3.11.174 + modern UI** |
|
||
| **11** | **2025-01-XX** | **Implemented configurable quality system (PdfViewerOptions + appsettings.json)** |
|
||
| **11** | **2025-01-XX** | **Added HiDPI/Retina support (4x quality on Retina displays)** |
|
||
| **11** | **2025-01-XX** | **Implemented thumbnail sidebar with resizable splitter (150-400px, localStorage)** |
|
||
| **11** | **2025-01-XX** | **Added smooth zoom transitions with configurable opacity and duration** |
|
||
| **11** | **2025-01-XX** | **Made zoom step configurable (buttons, Ctrl+Wheel, slider use same step)** |
|
||
| **11** | **2025-01-XX** | **Fixed thumbnail canvas alignment (object-fit: contain)** |
|
||
| **11** | **2025-01-XX** | **Fixed thumbnail re-rendering on sidebar toggle** |
|
||
|
||
---
|
||
|
||
## Layout Architecture (EnvelopeViewer)
|
||
|
||
### HTML Structure
|
||
```html
|
||
<div class="pdf-viewer-container">
|
||
<div class="pdf-toolbar">
|
||
<!-- Zoom, page navigation, thumbnail toggle -->
|
||
</div>
|
||
|
||
<div class="pdf-frame">
|
||
@if (_showThumbnails) {
|
||
<div class="pdf-thumbnails" style="width: @(_thumbnailWidth)px">
|
||
<!-- Page previews -->
|
||
</div>
|
||
<div class="pdf-splitter" @onmousedown="OnSplitterMouseDown">
|
||
<!-- Resizable divider -->
|
||
</div>
|
||
}
|
||
<div class="pdf-canvas-wrapper">
|
||
<canvas id="pdf-canvas" class="pdf-canvas"></canvas>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
### CSS Flexbox Layout
|
||
```css
|
||
.pdf-frame {
|
||
display: flex;
|
||
flex-direction: row; /* Side-by-side */
|
||
align-items: stretch; /* Same height */
|
||
overflow: hidden;
|
||
}
|
||
|
||
.pdf-thumbnails {
|
||
flex-shrink: 0; /* Fixed width */
|
||
width: 260px; /* Dynamic via inline style */
|
||
border-right: none; /* Seamless join with splitter */
|
||
}
|
||
|
||
.pdf-splitter {
|
||
width: 4px;
|
||
cursor: col-resize;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.pdf-canvas-wrapper {
|
||
flex: 1; /* Fill remaining space */
|
||
overflow: auto; /* Scrollable for zoom */
|
||
padding: 2rem;
|
||
text-align: center;
|
||
}
|
||
```
|
||
|
||
### 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()
|
||
|
||
3. Mouse move (global) ? OnSplitterMouseMove(clientX)
|
||
- Calculate delta = clientX - startX
|
||
- newWidth = startWidth + delta
|
||
- Clamp to 150-400px range
|
||
- Update _thumbnailWidth
|
||
- StateHasChanged() for reactive UI
|
||
|
||
4. Mouse up (global) ? OnSplitterMouseUp()
|
||
- _isResizing = false
|
||
- Remove 'resizing' class
|
||
- Save to localStorage("envelopeViewer_thumbnailWidth")
|
||
```
|
||
|
||
### 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
|
||
|
||
|