410349767d
T34c only fixed the bounded modal-surface offenders. Bootstrap 5.3's bg-light
and bg-white are fixed light values that do NOT flip under [data-bs-theme=dark],
so every remaining use was a dark-mode contrast break. 35 swaps across 19 files:
surface / <pre> / <code> bg-light -> bg-body-secondary
panel bg-white -> bg-body
neutral badge bg-light text-dark -> bg-secondary-subtle text-secondary-emphasis
muted badge / input group bg-light text-muted -> bg-body-secondary text-body-secondary
DELIBERATELY LEFT (7 sites): the neutral member of a status-badge switch or
ternary whose siblings are all solid, non-theme-aware colours (bg-success,
bg-danger, bg-warning) — swapping only the neutral one to a subtle token breaks
the visual weight of the set, so these stay until the whole family is retoned:
Topology.razor:513 (Current badge) and :588 (InstanceState.NotDeployed)
InstanceConfigure.razor:1520 (same InstanceState switch)
NotificationReport.razor StatusBadgeClass fallback
TransportImport.razor ConflictKind badge fallback
SecuredWrites.razor text-bg-light fallback (a text-bg-* family with no
theme-aware member at all)
Health.razor:377 depth ternary
Also untouched by design: SchemaBuilder.razor:88 (already bg-light-subtle,
theme-aware), bg-dark text-light console panels, and site.css / #reconnect-modal.
InstanceConfigure.razor is edited concurrently elsewhere; its change here is 5
pure class-string swaps on existing lines, no reflow. No test asserted any of
these classes. Also marks all four M10 residuals done in the plan doc.
186 lines
6.3 KiB
Plaintext
186 lines
6.3 KiB
Plaintext
@* Reusable diff/comparison dialog using Bootstrap modal.
|
|
Mirrors the ConfirmDialog API: callers invoke ShowAsync(title, before, after)
|
|
via @ref to display a side-by-side or simple before/after comparison.
|
|
z-index ladder follows ConfirmDialog: modal 1055 > backdrop 1040 (toasts at 1090). *@
|
|
@inject IJSRuntime JS
|
|
@inject ILogger<DiffDialog> Logger
|
|
@implements IAsyncDisposable
|
|
|
|
@if (_visible)
|
|
{
|
|
<div class="modal-backdrop fade show"></div>
|
|
<div @ref="_modalRef"
|
|
class="modal fade show d-block"
|
|
tabindex="-1"
|
|
role="dialog"
|
|
@onkeydown="OnKeyDownAsync">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">@Title</h5>
|
|
<button type="button" class="btn-close" aria-label="Close diff dialog" @onclick="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
@if (BodyContent != null)
|
|
{
|
|
@BodyContent
|
|
}
|
|
else
|
|
{
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<div class="small text-muted mb-1">Before</div>
|
|
<pre class="border rounded p-2 small bg-body-secondary mb-0" style="max-height: 50vh; overflow: auto; white-space: pre-wrap;">@Before</pre>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="small text-muted mb-1">After</div>
|
|
<pre class="border rounded p-2 small bg-body-secondary mb-0" style="max-height: 50vh; overflow: auto; white-space: pre-wrap;">@After</pre>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary btn-sm" @onclick="Close">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
private bool _visible;
|
|
private bool _bodyLocked;
|
|
private TaskCompletionSource<bool>? _tcs;
|
|
private ElementReference _modalRef;
|
|
|
|
[Parameter] public string Title { get; set; } = "Diff";
|
|
[Parameter] public string Before { get; set; } = string.Empty;
|
|
[Parameter] public string After { get; set; } = string.Empty;
|
|
/// <summary>
|
|
/// Optional custom body content. When supplied, it replaces the default
|
|
/// before/after panes — useful when the caller wants to render a richer
|
|
/// comparison (e.g. metadata badges, file lists, etc.).
|
|
/// </summary>
|
|
[Parameter] public RenderFragment? BodyContent { get; set; }
|
|
|
|
/// <summary>
|
|
/// Show the dialog with the supplied title and before/after text.
|
|
/// Returns when the user dismisses the dialog.
|
|
/// </summary>
|
|
public Task<bool> ShowAsync(string title, string before, string after)
|
|
{
|
|
Title = title;
|
|
Before = before;
|
|
After = after;
|
|
BodyContent = null;
|
|
return OpenAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Show the dialog with a custom body. Useful when the diff is not a
|
|
/// simple before/after string pair (e.g. a deployment comparison summary).
|
|
/// </summary>
|
|
public Task<bool> ShowAsync(string title, RenderFragment body)
|
|
{
|
|
Title = title;
|
|
BodyContent = body;
|
|
return OpenAsync();
|
|
}
|
|
|
|
private Task<bool> OpenAsync()
|
|
{
|
|
_visible = true;
|
|
_tcs = new TaskCompletionSource<bool>();
|
|
StateHasChanged();
|
|
return _tcs.Task;
|
|
}
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (_visible && !_bodyLocked)
|
|
{
|
|
_bodyLocked = true;
|
|
await TryLockBodyAsync();
|
|
try { await _modalRef.FocusAsync(); }
|
|
catch (InvalidOperationException)
|
|
{
|
|
// Prerender: the element reference is not attached yet — the
|
|
// next interactive render focuses it. Expected, not logged.
|
|
}
|
|
catch (JSDisconnectedException)
|
|
{
|
|
// Circuit gone before focus could run — nothing to do.
|
|
}
|
|
catch (JSException ex)
|
|
{
|
|
// A genuine focus interop failure (CentralUI-023) — log it.
|
|
Logger.LogWarning(ex, "DiffDialog: failed to focus the modal.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task OnKeyDownAsync(KeyboardEventArgs e)
|
|
{
|
|
if (e.Key == "Escape")
|
|
{
|
|
Close();
|
|
await Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
private void Close()
|
|
{
|
|
_visible = false;
|
|
_ = TryUnlockBodyAsync();
|
|
_tcs?.TrySetResult(true);
|
|
}
|
|
|
|
private async Task TryLockBodyAsync()
|
|
{
|
|
try
|
|
{
|
|
await JS.InvokeVoidAsync("document.body.classList.add", "modal-open");
|
|
}
|
|
catch (JSDisconnectedException)
|
|
{
|
|
// Circuit gone — the body scroll lock is moot. Expected, silent.
|
|
}
|
|
catch (JSException ex)
|
|
{
|
|
// CentralUI-023: a genuine interop failure — log instead of doing
|
|
// another (also-failing) JS call inside a bare catch.
|
|
Logger.LogWarning(ex, "DiffDialog: failed to apply body scroll lock.");
|
|
}
|
|
}
|
|
|
|
private async Task TryUnlockBodyAsync()
|
|
{
|
|
_bodyLocked = false;
|
|
try
|
|
{
|
|
await JS.InvokeVoidAsync("document.body.classList.remove", "modal-open");
|
|
}
|
|
catch (JSDisconnectedException)
|
|
{
|
|
// Circuit gone — the body scroll lock is moot. Expected, silent.
|
|
}
|
|
catch (JSException ex)
|
|
{
|
|
Logger.LogWarning(ex, "DiffDialog: failed to remove body scroll lock.");
|
|
}
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
// CentralUI-011: if the dialog is disposed while still open (the user
|
|
// navigated away), complete the pending task so the awaiting caller
|
|
// resumes deterministically instead of hanging forever.
|
|
_tcs?.TrySetResult(false);
|
|
|
|
if (_bodyLocked)
|
|
{
|
|
await TryUnlockBodyAsync();
|
|
}
|
|
}
|
|
}
|