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:
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user