From 715add7f51684c33e62fd99fccb634333da34b43 Mon Sep 17 00:00:00 2001 From: TekH Date: Mon, 5 Oct 2026 14:53:48 +0200 Subject: [PATCH] feat(sender): add create-envelope wizard with bootstrap handoff Introduce a 4-step modal wizard on SenderPage for creating envelopes. Validate required fields, support optional skip, receiver management, PDF upload, and keyboard shortcuts. Persist wizard payload in sessionStorage via token-based bootstrap service and navigate to editor with bootstrapToken. Register bootstrap storage service in both client and server DI containers and add dedicated sender wizard styling. --- .../Sender/CreateEnvelopeWizardPopup.razor | 659 ++++++++++++++++++ .../Sender/CreateEnvelopeWizardResult.cs | 12 + .../Pages/SenderPage.razor | 46 +- .../Program.cs | 1 + .../CreateEnvelopeBootstrapStorageService.cs | 54 ++ .../EnvelopeGenerator.Server/Program.cs | 1 + .../wwwroot/css/sender-page.css | 307 ++++++++ 7 files changed, 1079 insertions(+), 1 deletion(-) create mode 100644 EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Components/Sender/CreateEnvelopeWizardPopup.razor create mode 100644 EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Components/Sender/CreateEnvelopeWizardResult.cs create mode 100644 EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/CreateEnvelopeBootstrapStorageService.cs diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Components/Sender/CreateEnvelopeWizardPopup.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Components/Sender/CreateEnvelopeWizardPopup.razor new file mode 100644 index 00000000..95d21600 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Components/Sender/CreateEnvelopeWizardPopup.razor @@ -0,0 +1,659 @@ +@using EnvelopeGenerator.Application.Common.Dto +@using EnvelopeGenerator.Application.Resources +@using EnvelopeGenerator.Domain.Resources +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.Extensions.Localization +@inject IStringLocalizer ModelLocalizer +@inject IStringLocalizer UiLocalizer + + + +
+
+
+ @for (var step = 0; step < TotalSteps; step++) + { + var isActive = step == _currentStep; + var isDone = step < _currentStep; +
+ @(step + 1) +
+ } +
+ +
+
@CurrentStepTitle
+
@CurrentStepDescription
+
+ + @if (!string.IsNullOrWhiteSpace(_validationMessage)) + { +
@_validationMessage
+ } + + @if (_currentStep == 0) + { +
+
+ + +
+ +
+ + +
+
+ } + else if (_currentStep == 1) + { +
+
+ + +
+ +
+ + +
+
+ } + else if (_currentStep == 2) + { +
+
+ + +
+ +
+ + +
+
+ +
+ +
+ + @if (_receivers.Count == 0) + { +
+ @L("SenderEditor_NoReceiversAdded", "Noch keine Empfänger hinzugefügt.") +
+ } + else + { +
+ @foreach (var receiver in _receivers) + { +
+
+
@receiver.FullName
+
@receiver.EmailAddress
+
+ +
+ } +
+ } + } + else + { +
+ + + @if (_documentBytes is not null) + { +
+
@_documentFileName
+
@_documentSizeText
+
+ } + else + { +
+ @L("SenderWizard_DocumentRequiredHint", "Bitte laden Sie ein PDF-Dokument hoch, um fortzufahren.") +
+ } +
+ } +
+
+ + + +
+ +@code { + [Parameter] + public bool Visible { get; set; } + + [Parameter] + public EventCallback VisibleChanged { get; set; } + + [Parameter] + public EventCallback StartEditorRequested { get; set; } + + [Parameter] + public IReadOnlyList EnvelopeTypes { get; set; } = []; + + private const int TotalSteps = 4; + private static readonly System.ComponentModel.DataAnnotations.EmailAddressAttribute EmailValidator = new(); + + private bool _isVisible; + private bool _previousVisible; + private int _currentStep; + + private string _title = string.Empty; + private int? _selectedEnvelopeTypeId; + private string _message = string.Empty; + private string _language = "de-DE"; + + private string _receiverName = string.Empty; + private string _receiverEmail = string.Empty; + private readonly List _receivers = []; + + private byte[]? _documentBytes; + private string? _documentFileName; + private string _documentSizeText = string.Empty; + private bool _isDocumentUploading; + private bool _isStepActionRunning; + private bool _isFinishing; + + private string? _validationMessage; + + private bool IsActionLocked => _isDocumentUploading || _isStepActionRunning || _isFinishing; + + private bool IsFirstStep => _currentStep <= 0; + private bool IsLastStep => _currentStep >= TotalSteps - 1; + private bool CanSkipCurrentStep => _currentStep == 1; + + private string CurrentStepTitle => _currentStep switch + { + 0 => L("SenderWizard_Step_Basics", "Grunddaten"), + 1 => L("SenderWizard_Step_Message", "Nachricht und Optionen"), + 2 => L("SenderWizard_Step_Receivers", "Empfänger"), + 3 => L("SenderWizard_Step_Document", "Dokument"), + _ => L("SenderWizard_Step_Basics", "Grunddaten") + }; + + private string CurrentStepDescription => _currentStep switch + { + 0 => L("SenderWizard_Step_Basics_Desc", "Starten Sie mit den Pflichtangaben für den Umschlag."), + 1 => L("SenderWizard_Step_Message_Desc", "Fügen Sie optionale Nachricht und Einstellungen hinzu."), + 2 => L("SenderWizard_Step_Receivers_Desc", "Definieren Sie die Empfänger für den Signaturprozess."), + 3 => L("SenderWizard_Step_Document_Desc", "Laden Sie das Dokument hoch und platzieren Sie Signaturfelder."), + _ => L("SenderWizard_Step_Basics_Desc", "Starten Sie mit den Pflichtangaben für den Umschlag.") + }; + + protected override void OnParametersSet() + { + _isVisible = Visible; + + if (Visible && !_previousVisible) + ResetWizardState(); + + _previousVisible = Visible; + } + + private Task OnTitleChanged(string? value) + { + _title = value ?? string.Empty; + _validationMessage = null; + return Task.CompletedTask; + } + + private Task OnMessageChanged(string? value) + { + _message = value ?? string.Empty; + _validationMessage = null; + return Task.CompletedTask; + } + + private Task OnReceiverNameChanged(string? value) + { + _receiverName = value ?? string.Empty; + _validationMessage = null; + return Task.CompletedTask; + } + + private Task OnReceiverEmailChanged(string? value) + { + _receiverEmail = value ?? string.Empty; + _validationMessage = null; + return Task.CompletedTask; + } + + private Task OnEnvelopeTypeChanged(ChangeEventArgs args) + { + _validationMessage = null; + var raw = args.Value?.ToString(); + if (int.TryParse(raw, out var id) && id > 0) + { + _selectedEnvelopeTypeId = id; + return Task.CompletedTask; + } + + _selectedEnvelopeTypeId = null; + return Task.CompletedTask; + } + + private Task AddReceiver() + { + if (IsActionLocked) + return Task.CompletedTask; + + var fullName = _receiverName.Trim(); + var email = _receiverEmail.Trim(); + + if (string.IsNullOrWhiteSpace(fullName)) + { + _validationMessage = L("SenderEditor_Validation_FullNameRequired", "Bitte geben Sie einen Vor- und Nachnamen ein."); + return Task.CompletedTask; + } + + if (string.IsNullOrWhiteSpace(email)) + { + _validationMessage = L("SenderEditor_Validation_EmailRequired", "Bitte geben Sie eine E-Mail-Adresse ein."); + return Task.CompletedTask; + } + + if (!EmailValidator.IsValid(email)) + { + _validationMessage = L("SenderEditor_Validation_EmailInvalid", "Bitte geben Sie eine gültige E-Mail-Adresse ein."); + return Task.CompletedTask; + } + + if (_receivers.Any(x => string.Equals(x.EmailAddress, email, StringComparison.OrdinalIgnoreCase))) + { + _validationMessage = L("SenderEditor_Validation_EmailDuplicate", "Diese E-Mail-Adresse wurde bereits hinzugefügt."); + return Task.CompletedTask; + } + + _receivers.Add(new CreateEnvelopeWizardReceiver(fullName, email)); + _receiverName = string.Empty; + _receiverEmail = string.Empty; + _validationMessage = null; + return Task.CompletedTask; + } + + private Task RemoveReceiver(CreateEnvelopeWizardReceiver receiver) + { + if (IsActionLocked) + return Task.CompletedTask; + + _receivers.Remove(receiver); + _validationMessage = null; + return Task.CompletedTask; + } + + private async Task OnDocumentSelectedAsync(InputFileChangeEventArgs args) + { + if (IsActionLocked) + return; + + _validationMessage = null; + var file = args.File; + if (file is null) + return; + + if (!file.Name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)) + { + _validationMessage = L("SenderEditor_SelectPdf", "Bitte wählen Sie eine PDF-Datei aus."); + return; + } + + try + { + _isDocumentUploading = true; + + const long maxBytes = 50 * 1024 * 1024; + await using var stream = file.OpenReadStream(maxBytes); + using var ms = new System.IO.MemoryStream(); + await stream.CopyToAsync(ms); + + _documentBytes = ms.ToArray(); + _documentFileName = file.Name; + _documentSizeText = FormatSize(_documentBytes.Length); + } + finally + { + _isDocumentUploading = false; + } + } + + private Task MoveBack() + { + if (IsActionLocked) + return Task.CompletedTask; + + _validationMessage = null; + + if (!IsFirstStep) + _currentStep--; + + return Task.CompletedTask; + } + + private async Task MoveNextAsync() + { + if (IsActionLocked) + return; + + _isStepActionRunning = true; + try + { + if (!ValidateCurrentStep()) + return; + + _validationMessage = null; + + if (!IsLastStep) + _currentStep++; + } + finally + { + _isStepActionRunning = false; + } + + await Task.CompletedTask; + } + + private async Task SkipOptionalStep() + { + if (IsActionLocked) + return; + + if (!CanSkipCurrentStep) + return; + + _message = string.Empty; + _validationMessage = null; + await MoveNextAsync(); + } + + private async Task CloseAsync() + { + if (IsActionLocked) + return; + + await CloseCoreAsync(); + } + + private async Task CloseCoreAsync() + { + ResetWizardState(); + await OnPopupVisibleChangedAsync(false); + } + + private async Task FinishAsync() + { + if (IsActionLocked) + return; + + _isFinishing = true; + try + { + if (!ValidateCurrentStep()) + return; + + var result = new CreateEnvelopeWizardResult( + Title: _title.Trim(), + Message: _message.Trim(), + EnvelopeTypeId: _selectedEnvelopeTypeId, + Language: string.IsNullOrWhiteSpace(_language) ? "de-DE" : _language, + Receivers: _receivers.ToList(), + DocumentBytes: _documentBytes, + DocumentFileName: _documentFileName); + + await CloseCoreAsync(); + + if (StartEditorRequested.HasDelegate) + await StartEditorRequested.InvokeAsync(result); + } + finally + { + _isFinishing = false; + } + } + + private async Task OnWizardKeyDownAsync(KeyboardEventArgs args) + { + if (!_isVisible) + return; + + if (string.Equals(args.Key, "Escape", StringComparison.OrdinalIgnoreCase)) + { + await CloseAsync(); + return; + } + + if (!string.Equals(args.Key, "Enter", StringComparison.OrdinalIgnoreCase)) + return; + + if (_currentStep == 1) + return; + + if (_currentStep == 2 && (!string.IsNullOrWhiteSpace(_receiverName) || !string.IsNullOrWhiteSpace(_receiverEmail))) + { + await AddReceiver(); + return; + } + + if (IsLastStep) + { + await FinishAsync(); + return; + } + + await MoveNextAsync(); + } + + private async Task OnPopupVisibleChangedAsync(bool value) + { + _isVisible = value; + + if (!value) + ResetWizardState(); + + if (VisibleChanged.HasDelegate) + await VisibleChanged.InvokeAsync(value); + } + + private bool ValidateCurrentStep() + { + if (_currentStep == 0) + { + if (string.IsNullOrWhiteSpace(_title)) + { + _validationMessage = L("SenderEditor_Validation_TitleRequired", "Bitte geben Sie einen Titel ein."); + return false; + } + + if (!_selectedEnvelopeTypeId.HasValue || _selectedEnvelopeTypeId.Value <= 0) + { + _validationMessage = L("SenderWizard_Validation_EnvelopeTypeRequired", "Bitte wählen Sie einen Umschlagtyp aus."); + return false; + } + + return true; + } + + if (_currentStep == 2) + { + if (_receivers.Count == 0) + { + _validationMessage = L("SenderEditor_Validation_ReceiverRequired", "Bitte fügen Sie mindestens einen Empfänger hinzu."); + return false; + } + + return true; + } + + if (_currentStep == 3) + { + if (_documentBytes is null || _documentBytes.Length == 0) + { + _validationMessage = L("SenderEditor_Validation_UploadPdf", "Bitte laden Sie zuerst ein PDF-Dokument hoch."); + return false; + } + + return true; + } + + return true; + } + + private static string FormatSize(long bytes) + { + const double kb = 1024; + const double mb = kb * 1024; + + if (bytes >= mb) + return $"{bytes / mb:0.##} MB"; + + if (bytes >= kb) + return $"{bytes / kb:0.##} KB"; + + return $"{bytes} B"; + } + + private void ResetWizardState() + { + _currentStep = 0; + _validationMessage = null; + + _title = string.Empty; + _selectedEnvelopeTypeId = EnvelopeTypes.Count > 0 ? EnvelopeTypes[0].Id : null; + _message = string.Empty; + _language = "de-DE"; + + _receiverName = string.Empty; + _receiverEmail = string.Empty; + _receivers.Clear(); + + _documentBytes = null; + _documentFileName = null; + _documentSizeText = string.Empty; + } + + private string L(string key, string fallback) + { + var modelValue = ModelLocalizer[key]; + if (!modelValue.ResourceNotFound) + return modelValue.Value; + + var uiValue = UiLocalizer[key]; + return uiValue.ResourceNotFound ? fallback : uiValue.Value; + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Components/Sender/CreateEnvelopeWizardResult.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Components/Sender/CreateEnvelopeWizardResult.cs new file mode 100644 index 00000000..d81c6f9b --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Components/Sender/CreateEnvelopeWizardResult.cs @@ -0,0 +1,12 @@ +namespace EnvelopeGenerator.Server.Client.Components.Sender; + +public sealed record CreateEnvelopeWizardResult( + string Title, + string Message, + int? EnvelopeTypeId, + string Language, + IReadOnlyList Receivers, + byte[]? DocumentBytes, + string? DocumentFileName); + +public sealed record CreateEnvelopeWizardReceiver(string FullName, string EmailAddress); diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/SenderPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/SenderPage.razor index 5bc23b24..1c07de46 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/SenderPage.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/SenderPage.razor @@ -9,14 +9,17 @@ @using EnvelopeGenerator.Application.Common.Dto.History @using EnvelopeGenerator.Application.Resources @using EnvelopeGenerator.Domain.Resources +@using EnvelopeGenerator.Server.Client.Components.Sender @using Microsoft.Extensions.Localization @inject EnvelopeGenerator.Server.Client.Services.EnvelopeService EnvelopeService @inject EnvelopeGenerator.Server.Client.Services.DocumentService DocumentService @inject EnvelopeGenerator.Server.Client.Services.PdfRenderService PdfRenderService +@inject EnvelopeGenerator.Server.Client.Services.EnvelopeTypeService EnvelopeTypeService @inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService @inject EnvelopeGenerator.Server.Client.Services.ThemeService ThemeService @inject EnvelopeGenerator.Server.Client.Services.UserPreferencesService UserPreferencesService @inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService +@inject EnvelopeGenerator.Server.Client.Services.CreateEnvelopeBootstrapStorageService CreateEnvelopeBootstrapStorageService @inject ILogger Logger @inject NavigationManager Navigation @inject IJSRuntime JSRuntime @@ -414,6 +417,11 @@ + + @* ── PDF Vorschau Popup ────────────────────────────────────────────────── *@ _envelopeTypes = []; + // Stable keys used to store/restore each tab's layout in UserPreferences.GridLayouts private const string GridKeyActive = "sender.active-envelopes"; private const string GridKeyCompleted = "sender.completed-envelopes"; @@ -596,6 +607,16 @@ _appLogo = logos.App; _companyLogo = logos.Company; + try + { + _envelopeTypes = (await EnvelopeTypeService.GetAsync()).ToList(); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to load envelope types for create wizard."); + _envelopeTypes = []; + } + await LoadEnvelopesAsync(); } @@ -693,7 +714,30 @@ void CreateEnvelope() { - Navigation.NavigateTo("/sender/editor"); + _createWizardVisible = true; + } + + Task OnCreateWizardVisibleChangedAsync(bool visible) + { + _createWizardVisible = visible; + return Task.CompletedTask; + } + + async Task StartEditorFromWizardAsync(CreateEnvelopeWizardResult result) + { + var payload = new CreateEnvelopeBootstrapPayload( + Title: result.Title, + Message: result.Message, + EnvelopeTypeId: result.EnvelopeTypeId, + Language: result.Language, + Receivers: result.Receivers + .Select(x => new CreateEnvelopeBootstrapReceiver(x.FullName, x.EmailAddress)) + .ToList(), + DocumentBytes: result.DocumentBytes, + DocumentFileName: result.DocumentFileName); + + var token = await CreateEnvelopeBootstrapStorageService.StoreAsync(payload); + Navigation.NavigateTo($"/sender/editor?bootstrapToken={token}"); } void EditEnvelope() diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Program.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Program.cs index f53e1c8d..b221ad7e 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Program.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Program.cs @@ -45,6 +45,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // UI Preferences & Theme builder.Services.AddScoped(); diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/CreateEnvelopeBootstrapStorageService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/CreateEnvelopeBootstrapStorageService.cs new file mode 100644 index 00000000..e39fd8a4 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/CreateEnvelopeBootstrapStorageService.cs @@ -0,0 +1,54 @@ +using System.Text.Json; +using Microsoft.JSInterop; + +namespace EnvelopeGenerator.Server.Client.Services; + +public sealed class CreateEnvelopeBootstrapStorageService(IJSRuntime jsRuntime) +{ + private const string StorageKeyPrefix = "sender.createEnvelope.bootstrap."; + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); + + public async Task StoreAsync(CreateEnvelopeBootstrapPayload payload) + { + var token = Guid.NewGuid().ToString("N"); + var key = BuildStorageKey(token); + var json = JsonSerializer.Serialize(payload, JsonOptions); + await jsRuntime.InvokeVoidAsync("sessionStorage.setItem", key, json); + return token; + } + + public async Task ConsumeAsync(string? token) + { + if (string.IsNullOrWhiteSpace(token)) + return null; + + var key = BuildStorageKey(token); + var raw = await jsRuntime.InvokeAsync("sessionStorage.getItem", key); + await jsRuntime.InvokeVoidAsync("sessionStorage.removeItem", key); + + if (string.IsNullOrWhiteSpace(raw)) + return null; + + try + { + return JsonSerializer.Deserialize(raw, JsonOptions); + } + catch + { + return null; + } + } + + private static string BuildStorageKey(string token) => $"{StorageKeyPrefix}{token}"; +} + +public sealed record CreateEnvelopeBootstrapPayload( + string Title, + string Message, + int? EnvelopeTypeId, + string Language, + IReadOnlyList Receivers, + byte[]? DocumentBytes, + string? DocumentFileName); + +public sealed record CreateEnvelopeBootstrapReceiver(string FullName, string EmailAddress); diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs index 020de466..9c8cb60d 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs @@ -346,6 +346,7 @@ try builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); // Business Services (Server specific) builder.Services.AddScoped(); diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/sender-page.css b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/sender-page.css index dce4848c..6d715ce4 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/sender-page.css +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/sender-page.css @@ -1094,3 +1094,310 @@ [data-sf-theme="blazing-dark"] .sender-preview-error-msg { color: rgba(255, 255, 255, 0.50); } + +.sender-create-wizard { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 0.25rem 0; + outline: none; +} + +.sender-create-wizard-popup .dxbl-popup-content { + max-height: min(78vh, 760px); + overflow: auto; +} + +.sender-create-wizard-popup .dxbl-popup-footer { + position: sticky; + bottom: 0; + background: rgba(255, 255, 255, 0.96); + backdrop-filter: blur(6px); + z-index: 2; +} + +.sender-create-wizard__progress { + position: relative; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + align-items: center; + gap: 0.5rem; +} + +.sender-create-wizard__progress-track { + position: absolute; + left: 12px; + right: 12px; + top: 50%; + height: 2px; + transform: translateY(-50%); + background: rgba(126, 34, 206, 0.18); + z-index: 0; +} + +.sender-create-wizard__step { + position: relative; + z-index: 1; + display: flex; + justify-content: center; +} + +.sender-create-wizard__step-index { + width: 24px; + height: 24px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 0.78rem; + font-weight: 700; + color: #64748b; + background: #f8fafc; + border: 1px solid rgba(126, 34, 206, 0.25); +} + +.sender-create-wizard__step--active .sender-create-wizard__step-index { + color: #ffffff; + background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%); + border-color: transparent; + box-shadow: 0 0 0 3px rgba(126, 34, 206, 0.15); +} + +.sender-create-wizard__step--done .sender-create-wizard__step-index { + color: #ffffff; + background: #2a5298; + border-color: transparent; +} + +.sender-create-wizard__meta { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.sender-create-wizard__title { + font-size: 1rem; + font-weight: 700; + color: #1f2937; +} + +.sender-create-wizard__description { + font-size: 0.875rem; + color: #64748b; + line-height: 1.4; +} + +.sender-create-wizard__stage { + border: 1px solid rgba(126, 34, 206, 0.12); + border-radius: 10px; + background: rgba(126, 34, 206, 0.04); + padding: 0.85rem 0.95rem; +} + +.sender-create-wizard__stage p { + margin: 0; + color: #334155; + font-size: 0.9rem; +} + +.sender-create-wizard__footer { + width: 100%; + display: flex; + justify-content: flex-end; + gap: 0.5rem; +} + +@media (max-width: 640px) { + .sender-create-wizard__footer { + flex-wrap: wrap; + } + + .sender-create-wizard__footer .dxbl-btn { + flex: 1 1 calc(50% - 0.5rem); + } +} + +.sender-create-wizard__validation { + border: 1px solid rgba(220, 38, 38, 0.24); + background: rgba(254, 226, 226, 0.9); + color: #991b1b; + border-radius: 8px; + padding: 0.6rem 0.75rem; + font-size: 0.85rem; +} + +.sender-create-wizard__form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.9rem; +} + +.sender-create-wizard__field { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.sender-create-wizard__field--full { + grid-column: 1 / -1; +} + +.sender-create-wizard__label { + font-size: 0.82rem; + font-weight: 700; + color: #334155; +} + +.sender-create-wizard__required { + color: #dc2626; +} + +.sender-create-wizard__optional { + color: #64748b; + font-weight: 600; +} + +.sender-create-wizard__select { + min-height: 38px; + border: 1px solid rgba(126, 34, 206, 0.25); + border-radius: 8px; + padding: 0.45rem 0.55rem; + background: #ffffff; + color: #1f2937; +} + +.sender-create-wizard__receiver-actions { + display: flex; + justify-content: flex-end; +} + +.sender-create-wizard__receiver-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.sender-create-wizard__receiver-item { + display: flex; + align-items: center; + justify-content: space-between; + border: 1px solid rgba(126, 34, 206, 0.18); + border-radius: 8px; + padding: 0.55rem 0.7rem; + background: rgba(248, 250, 252, 0.95); + gap: 0.75rem; +} + +.sender-create-wizard__receiver-name { + font-size: 0.86rem; + font-weight: 700; + color: #1f2937; +} + +.sender-create-wizard__receiver-email { + font-size: 0.79rem; + color: #64748b; +} + +.sender-create-wizard__receiver-remove { + border: 1px solid rgba(220, 38, 38, 0.2); + border-radius: 6px; + background: rgba(220, 38, 38, 0.08); + color: #b91c1c; + font-size: 0.78rem; + font-weight: 700; + padding: 0.25rem 0.55rem; +} + +.sender-create-wizard__upload-stage { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.sender-create-wizard__upload-btn { + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid rgba(126, 34, 206, 0.28); + border-radius: 8px; + padding: 0.55rem 0.9rem; + background: rgba(126, 34, 206, 0.08); + color: #4c1d95; + font-size: 0.84rem; + font-weight: 700; + width: fit-content; + cursor: pointer; +} + +.sender-create-wizard__upload-btn--disabled { + opacity: 0.55; + cursor: not-allowed; +} + +.sender-create-wizard__hidden-input { + display: none; +} + +.sender-create-wizard__document-meta { + border: 1px solid rgba(16, 185, 129, 0.3); + border-radius: 8px; + background: rgba(16, 185, 129, 0.08); + padding: 0.55rem 0.7rem; +} + +.sender-create-wizard__document-name { + color: #064e3b; + font-size: 0.85rem; + font-weight: 700; +} + +.sender-create-wizard__document-size { + color: #065f46; + font-size: 0.77rem; +} + +.sender-create-wizard__empty-list { + border: 1px dashed rgba(100, 116, 139, 0.4); + border-radius: 8px; + padding: 0.6rem 0.75rem; + color: #475569; + font-size: 0.83rem; + background: rgba(248, 250, 252, 0.8); +} + +@media (max-width: 760px) { + .sender-create-wizard-popup .dxbl-popup { + width: 96vw !important; + max-width: 96vw !important; + } + + .sender-create-wizard__progress { + gap: 0.35rem; + } + + .sender-create-wizard__title { + font-size: 0.93rem; + } + + .sender-create-wizard__description { + font-size: 0.81rem; + } + + .sender-create-wizard__form-grid { + grid-template-columns: 1fr; + } + + .sender-create-wizard__receiver-item { + flex-direction: column; + align-items: flex-start; + } + + .sender-create-wizard__receiver-remove { + width: 100%; + } + + .sender-create-wizard__upload-btn { + width: 100%; + } +}