refactor(sender): overhaul PDF preview popup layout and styling

Replace inline styles and magic-number height calculations with a proper
BEM CSS class system. Attach HeaderCssClass and BodyCssClass to DxPopup
so DevExpress DOM targets can be styled reliably without deep selector
hacks.

Layout changes:
- Remove hardcoded calc(90vh - 114px) height from viewer wrapper; use
  flex layout so the body fills available space naturally
- Change overflow on DxPdfViewer wrapper from hidden to auto to allow
  horizontal scrolling at high zoom levels
- Center PDF pages horizontally inside dxbrv-document-surface via flex

New CSS classes (BEM):
- .sender-preview-popup           root shell, accent top border
- .sender-preview-popup__header   styled header via HeaderCssClass
- .sender-preview-popup__body     flex column body via BodyCssClass
- .sender-preview-viewer-wrap     padding wrapper for the PDF viewer
- .sender-preview-pdf-viewer      viewer shell with rounded corners / shadow
- .sender-preview-state-wrap      centered container for loading/error states
- .sender-preview-error-card      error message card with left accent border
- .sender-preview-error-icon      circular icon wrapper
- .sender-preview-error-title/msg typography helpers

Theme support:
- Purple, Blazing Dark variants via data-sf-theme attribute overrides

Custom scrollbars:
- WebKit scrollbar styling scoped to .sender-preview-popup__body

Inline style cleanup in EnvelopeSenderPage.razor:
- Replace all ad-hoc style= attributes with the new CSS classes above
This commit is contained in:
2026-09-24 12:02:22 +02:00
parent 32a9d008a4
commit 1ad673904a
2 changed files with 303 additions and 34 deletions

View File

@@ -441,6 +441,8 @@
CloseOnOutsideClick="true"
ShowFooter="false"
CssClass="sender-preview-popup"
HeaderCssClass="sender-preview-popup__header"
BodyCssClass="sender-preview-popup__body"
Closed="OnPreviewClosed"
AllowResize="true"
ApplyBackgroundShading="true"
@@ -451,7 +453,7 @@
<BodyContentTemplate>
@if (_previewLoading)
{
<div style="display:flex;justify-content:center;align-items:center;height:100%;min-height:300px;">
<div class="sender-preview-state-wrap">
<div class="text-center">
<div class="spinner-border mb-3" style="width:3rem;height:3rem;" role="status">
<span class="visually-hidden">Lädt...</span>
@@ -462,27 +464,24 @@
}
else if (_previewErrorMessage is not null)
{
<div style="display:flex;gap:1rem;align-items:flex-start;padding:1.5rem;">
<div style="flex-shrink:0;width:40px;height:40px;border-radius:50%;
background:#fee2e2;display:flex;align-items:center;justify-content:center;">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#dc2626" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
</svg>
</div>
<div>
<p style="margin:0 0 0.4rem;font-weight:600;color:#1f2937;font-size:0.95rem;">
Dokument konnte nicht geladen werden
</p>
<p style="margin:0;color:#6b7280;font-size:0.85rem;line-height:1.5;word-break:break-word;">
@_previewErrorMessage
</p>
<div class="sender-preview-state-wrap">
<div class="sender-preview-error-card">
<div class="sender-preview-error-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#dc2626" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
</svg>
</div>
<div>
<p class="sender-preview-error-title">Dokument konnte nicht geladen werden</p>
<p class="sender-preview-error-msg">@_previewErrorMessage</p>
</div>
</div>
</div>
}
else if (_previewPdfBytes is not null && _previewViewerMounted)
{
<div class="sender-preview-viewer-wrap" style="padding: 0 5.5rem;">
<div class="sender-preview-viewer-wrap">
<DxPdfViewer DocumentContent="@_previewPdfBytes"
ZoomLevel="-2"
IsSinglePagePreview="true"

View File

@@ -481,35 +481,305 @@
}
/* ── PDF Preview Popup ─────────────────────────────────────────────────── */
/*
* DxPdfViewer needs an explicit pixel-height parent — percentage heights
* resolve to 0 inside popup body flex containers and cause DevExpress
* ProcessDocumentChangedAsync NullReferenceExceptions.
* HOW DEVEXPRESS POPUP CSS WORKS:
*
* Strategy:
* - Popup: 80vw × 90vh
* - Body: fills remaining space between header (~52px) and footer (~58px)
* - Wrapper: flex column, explicit height = calc(90vh - 52px - 58px - 2px)
* - DxPdfViewer: flex:1, overflow handled internally by DevExpress (scroll works)
* CssClass="sender-preview-popup"
* → added to the popup's root <div> element (the outermost element)
* → use selector: .sender-preview-popup { ... }
*
* HeaderCssClass="sender-preview-popup__header"
* → added directly to the header element
* → use selector: .sender-preview-popup__header { ... }
*
* BodyCssClass="sender-preview-popup__body"
* → added directly to the body element
* → use selector: .sender-preview-popup__body { ... }
*
* NOTE: .sender-preview-popup .dxbl-modal::before does NOT work because
* CssClass is on the root, NOT on a parent of dxbl-modal.
* Instead use the root element itself with pseudo-elements or direct selectors.
*
* SCROLLING:
* Scrollable="true" → DevExpress adds vertical scrollbar.
* For horizontal scroll we override the body's overflow-x.
*
* DESIGN LANGUAGE:
* - Purple→blue gradient accent top border (same as .sender-grid-container::before)
* - Glassmorphism surface
* - Consistent with sender-page design system
*/
.sender-preview-popup .dxbl-popup-body {
padding: 0 !important;
overflow: hidden;
/* ── Popup root shell ────────────────────────────────────────────────────── */
/* Accent stripe at top — uses outline trick since ::before on CssClass root works */
.sender-preview-popup {
border-top: 4px solid transparent;
border-image: linear-gradient(90deg, #7e22ce 0%, #2a5298 100%) 1;
box-shadow:
0 32px 64px -12px rgba(0, 0, 0, 0.35),
0 0 0 1px rgba(126, 34, 206, 0.12) !important;
border-radius: 20px !important;
overflow: hidden !important;
}
/* ── Header (via HeaderCssClass) ─────────────────────────────────────────── */
.sender-preview-popup__header {
background: rgba(255, 255, 255, 0.97) !important;
backdrop-filter: blur(20px) !important;
border-bottom: 1px solid rgba(126, 34, 206, 0.14) !important;
padding: 0.875rem 1.5rem !important;
}
/* Header title text */
.sender-preview-popup__header .dxbl-popup-header-text {
font-size: 0.9375rem !important;
font-weight: 700 !important;
color: #1e293b !important;
letter-spacing: -0.02em !important;
}
/* Close button inside header */
.sender-preview-popup__header .dxbl-btn {
border-radius: 8px !important;
color: #64748b !important;
transition: all 0.18s ease !important;
}
.sender-preview-popup__header .dxbl-btn:hover {
background: rgba(126, 34, 206, 0.08) !important;
color: #7e22ce !important;
}
/* ── Body (via BodyCssClass) ─────────────────────────────────────────────── */
.sender-preview-popup__body {
padding: 0 !important;
background: #f8fafc !important;
overflow: auto !important; /* both axes — enables horizontal scroll */
overflow-x: auto !important;
display: flex !important;
flex-direction: column !important;
}
/* ── PDF viewer wrapper ──────────────────────────────────────────────────── */
/*
* Scrollable=true on DxPopup adds a vertical scroll to the body.
* The wrapper fills the body via flex and adds breathing-room padding.
* min-width: 0 prevents flex children from pushing the wrapper wider
* than the popup — which would kill horizontal scroll at the body level.
*/
.sender-preview-viewer-wrap {
flex: 1 1 auto;
display: flex;
flex-direction: column;
width: 100%;
height: calc(90vh - 114px); /* 52px header + 58px footer + 4px borders */
min-height: 480px;
overflow: hidden; /* inner DxPdfViewer manages its own scroll */
min-height: 0;
padding: 1rem 1.5rem 1.25rem;
box-sizing: border-box;
}
/* ── DxPdfViewer instance ────────────────────────────────────────────────── */
/*
* DO NOT set overflow:hidden here — it would clip the PDF at high zoom.
* overflow:auto lets horizontal scroll appear when the PDF page is wider
* than the viewer container (e.g. 100% zoom on a narrow popup).
*/
.sender-preview-pdf-viewer {
flex: 1 1 auto;
width: 100%;
height: 100%;
overflow: auto;
min-height: 0;
border-radius: 12px;
overflow: auto; /* was: hidden — changed to allow h-scroll */
box-shadow:
0 4px 24px rgba(0, 0, 0, 0.10),
0 0 0 1px rgba(126, 34, 206, 0.10);
}
/*
* DevExpress PDF viewer internal DOM (confirmed from browser DevTools):
*
* .dxbrv-wrapper ← CssClass="sender-preview-pdf-viewer"
* .dxbrv-surface-container ← width: calc(100% + 0rem) — fills wrap
* .dxbrv-gridlayout-content
* .dxbrv-surface-wrapper
* .dxbrv-toolbar ← page nav / zoom bar
* .dxbrv-viewbox ← THE scroll container for the document
* .dxbrv-document-surface
* .dxbrv-report-preview-content-flex-item
* .dxbrv-report-preview-content ← width: 1133px (at 100%)
*
* Goal:
* • Center the PDF page horizontally when it is narrower than the viewbox.
* • Allow horizontal scrollbar when the page is wider than the viewbox.
* • Keep vertical scroll handled by DxPopup Scrollable=true (body level).
*/
/* Allow both axes of scroll on the DevExpress viewbox */
.sender-preview-pdf-viewer .dxbrv-viewbox {
overflow: auto !important;
}
/* Center pages horizontally inside the viewbox */
.sender-preview-pdf-viewer .dxbrv-document-surface {
display: flex !important;
flex-direction: column !important;
align-items: center !important;
min-width: min-content !important; /* shrink-wraps to page width at high zoom */
}
.sender-preview-pdf-viewer .dxbrv-report-preview-content-flex-item {
display: flex !important;
justify-content: center !important;
width: 100% !important;
}
.sender-preview-pdf-viewer .dxbrv-report-preview-content {
margin-left: auto !important;
margin-right: auto !important;
}
/* ── Custom scrollbars (WebKit) ─────────────────────────────────────────── */
.sender-preview-popup__body *::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.sender-preview-popup__body *::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.04);
border-radius: 4px;
}
.sender-preview-popup__body *::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%);
border-radius: 4px;
}
.sender-preview-popup__body *::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #6b1cb0 0%, #1e3a72 100%);
}
/* ── Loading / error states ──────────────────────────────────────────────── */
.sender-preview-state-wrap {
flex: 1 1 auto;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
min-height: 300px;
}
.sender-preview-error-card {
display: flex;
gap: 1rem;
align-items: flex-start;
background: #fff1f2;
border: 1px solid rgba(220, 38, 38, 0.2);
border-left: 4px solid #e11d48;
border-radius: 12px;
padding: 1.25rem 1.5rem;
max-width: 560px;
box-shadow: 0 4px 16px rgba(220, 38, 38, 0.08);
}
.sender-preview-error-icon {
flex-shrink: 0;
width: 40px;
height: 40px;
border-radius: 50%;
background: #fee2e2;
display: flex;
align-items: center;
justify-content: center;
}
.sender-preview-error-title {
margin: 0 0 0.4rem;
font-weight: 700;
color: #1f2937;
font-size: 0.95rem;
}
.sender-preview-error-msg {
margin: 0;
color: #6b7280;
font-size: 0.85rem;
line-height: 1.5;
word-break: break-word;
}
/* ── Theme: Purple ───────────────────────────────────────────────────────── */
[data-sf-theme="purple"] .sender-preview-popup {
border-image: linear-gradient(90deg, #9333ea 0%, #6b21a8 100%) 1;
}
[data-sf-theme="purple"] .sender-preview-popup__header {
border-bottom-color: rgba(147, 51, 234, 0.15) !important;
}
[data-sf-theme="purple"] .sender-preview-popup__header .dxbl-btn:hover {
background: rgba(147, 51, 234, 0.08) !important;
color: #9333ea !important;
}
[data-sf-theme="purple"] .sender-preview-pdf-viewer {
box-shadow:
0 4px 24px rgba(0, 0, 0, 0.10),
0 0 0 1px rgba(147, 51, 234, 0.12);
}
/* ── Theme: Blazing Dark ─────────────────────────────────────────────────── */
[data-sf-theme="blazing-dark"] .sender-preview-popup {
border-image: linear-gradient(90deg, #6366f1 0%, #4338ca 100%) 1;
box-shadow:
0 32px 64px -12px rgba(0, 0, 0, 0.6),
0 0 0 1px rgba(99, 102, 241, 0.18) !important;
}
[data-sf-theme="blazing-dark"] .sender-preview-popup__header {
background: rgba(15, 20, 40, 0.96) !important;
border-bottom-color: rgba(99, 102, 241, 0.18) !important;
}
[data-sf-theme="blazing-dark"] .sender-preview-popup__header .dxbl-popup-header-text {
color: rgba(255, 255, 255, 0.92) !important;
}
[data-sf-theme="blazing-dark"] .sender-preview-popup__header .dxbl-btn {
color: rgba(255, 255, 255, 0.50) !important;
}
[data-sf-theme="blazing-dark"] .sender-preview-popup__header .dxbl-btn:hover {
background: rgba(99, 102, 241, 0.12) !important;
color: #818cf8 !important;
}
[data-sf-theme="blazing-dark"] .sender-preview-popup__body {
background: #0d1117 !important;
}
[data-sf-theme="blazing-dark"] .sender-preview-pdf-viewer {
box-shadow:
0 4px 24px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(99, 102, 241, 0.15);
}
[data-sf-theme="blazing-dark"] .sender-preview-state-wrap {
color: rgba(255, 255, 255, 0.7);
}
[data-sf-theme="blazing-dark"] .sender-preview-error-card {
background: rgba(239, 68, 68, 0.10);
border-color: rgba(239, 68, 68, 0.25);
border-left-color: rgba(239, 68, 68, 0.6);
}
[data-sf-theme="blazing-dark"] .sender-preview-error-icon {
background: rgba(239, 68, 68, 0.15);
}
[data-sf-theme="blazing-dark"] .sender-preview-error-title {
color: rgba(255, 255, 255, 0.88);
}
[data-sf-theme="blazing-dark"] .sender-preview-error-msg {
color: rgba(255, 255, 255, 0.50);
}