mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-04 04:18:13 +10:00
Shared StatusBadge, OK/Cancel/Revert, fixes, badge styling
- Add Components/Shared/StatusBadge + StatusBadgeKind; centralize badge styles and map Kind to Bootstrap classes. Refactor OptionStateBadge to compose StatusBadge; add OK/Cancel/Revert when active with OnConfirm/OnCancel/OnRevert callbacks and exit-action flag in EffectiveConfigFieldDisplay. - Fix _valueAtActivation to use Descriptor.GetValue() (persisted value) so save modal shows correct before/after and no phantom pending on revert-to-original. - OptionHelpController: add CancelCloseFromLabelEnter() and call when opening/switching help so pending 300ms close does not dismiss new help. - Migrate Dnsmasq.razor and HostsFileSection to StatusBadge; save modal option chip uses NeutralPill. Centralize badge CSS in StatusBadge. - Badge styling: font-size/weight/padding, hover lift+shadow, active press with visible release animation.
This commit is contained in:
+27
-9
@@ -34,7 +34,7 @@
|
|||||||
@fragment(Descriptor)
|
@fragment(Descriptor)
|
||||||
</CascadingValue>
|
</CascadingValue>
|
||||||
}
|
}
|
||||||
<OptionStateBadge AnchorId="@_readonlyBadgeAnchorId" Source="@Descriptor.GetSource()" OptionName="@Descriptor.OptionName" Value="@effectiveValue" IsInteractive="@IsEditMode" ShowEditableBadge="@showEditableBadge" HasPendingChange="@_hasPendingChange" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="@isActiveEditor" OnCancelRequested="@HandleCancelEdit" />
|
<OptionStateBadge AnchorId="@_readonlyBadgeAnchorId" Source="@Descriptor.GetSource()" OptionName="@Descriptor.OptionName" Value="@effectiveValue" IsInteractive="@IsEditMode" ShowEditableBadge="@showEditableBadge" HasPendingChange="@_hasPendingChange" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="@isActiveEditor" OnConfirmRequested="@HandleConfirmEdit" OnCancelRequested="@HandleCancelEdit" OnRevertRequested="@HandleRevertEdit" />
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ else
|
|||||||
<EffectiveConfigOptionItem Label="@Descriptor.OptionName" DisplayValue="@(displayValue ?? "(not set)")" Source="@Descriptor.GetSource()" LabelTooltip="@_labelTooltip" OptionHelpKey="@_optionHelpKey" OptionHelpAnchorId="@_anchorId"
|
<EffectiveConfigOptionItem Label="@Descriptor.OptionName" DisplayValue="@(displayValue ?? "(not set)")" Source="@Descriptor.GetSource()" LabelTooltip="@_labelTooltip" OptionHelpKey="@_optionHelpKey" OptionHelpAnchorId="@_anchorId"
|
||||||
ShowEditableBadge="true" FieldKey="@FieldKey" IsActive="@IsActive"
|
ShowEditableBadge="true" FieldKey="@FieldKey" IsActive="@IsActive"
|
||||||
ReadonlyBadgeAnchorId="@_readonlyBadgeAnchorId" RawValue="@value" HasPendingChange="@_hasPendingChange"
|
ReadonlyBadgeAnchorId="@_readonlyBadgeAnchorId" RawValue="@value" HasPendingChange="@_hasPendingChange"
|
||||||
DisplayValueChanged="@HandleDefaultValueChanged" OnBlur="@HandleDefaultBlur" OnCancelRequested="@HandleCancelEdit" />
|
DisplayValueChanged="@HandleDefaultValueChanged" OnBlur="@HandleDefaultBlur" OnConfirmRequested="@HandleConfirmEdit" OnCancelRequested="@HandleCancelEdit" OnRevertRequested="@HandleRevertEdit" />
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ else
|
|||||||
private object? _valueAtActivation;
|
private object? _valueAtActivation;
|
||||||
private object? _draftValue;
|
private object? _draftValue;
|
||||||
private bool _wasActive;
|
private bool _wasActive;
|
||||||
private bool _cancelRequested;
|
private ExitAction _exitAction = ExitAction.None;
|
||||||
|
|
||||||
protected override void OnParametersSet()
|
protected override void OnParametersSet()
|
||||||
{
|
{
|
||||||
@@ -100,11 +100,11 @@ else
|
|||||||
_hasPendingChange = pending != null;
|
_hasPendingChange = pending != null;
|
||||||
_effectiveValue = pending != null ? pending.NewValue : Descriptor.GetValue();
|
_effectiveValue = pending != null ? pending.NewValue : Descriptor.GetValue();
|
||||||
if (!_wasActive && IsActive)
|
if (!_wasActive && IsActive)
|
||||||
_valueAtActivation = _effectiveValue;
|
_valueAtActivation = Descriptor.GetValue();
|
||||||
if (_wasActive && !IsActive && !_cancelRequested)
|
if (_wasActive && !IsActive && _exitAction == ExitAction.None)
|
||||||
_ = InvokeAsync(CommitIfChangedAsync);
|
_ = InvokeAsync(CommitIfChangedAsync);
|
||||||
if (_wasActive && !IsActive)
|
if (_wasActive && !IsActive)
|
||||||
_cancelRequested = false;
|
_exitAction = ExitAction.None;
|
||||||
_wasActive = IsActive;
|
_wasActive = IsActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,12 +146,30 @@ else
|
|||||||
await CommitIfChangedAsync();
|
await CommitIfChangedAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleCancelEdit()
|
private async Task HandleConfirmEdit()
|
||||||
{
|
{
|
||||||
_cancelRequested = true;
|
await CommitIfChangedAsync();
|
||||||
_draftValue = null;
|
_exitAction = ExitAction.Confirm;
|
||||||
if (Ui != null)
|
if (Ui != null)
|
||||||
await Ui.DeactivateFieldAsync();
|
await Ui.DeactivateFieldAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task HandleCancelEdit()
|
||||||
|
{
|
||||||
|
_draftValue = null;
|
||||||
|
_exitAction = ExitAction.Cancel;
|
||||||
|
if (Ui != null)
|
||||||
|
await Ui.DeactivateFieldAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleRevertEdit()
|
||||||
|
{
|
||||||
|
_draftValue = null;
|
||||||
|
Session?.RevertChange(Descriptor.SectionId, Descriptor.OptionName);
|
||||||
|
_exitAction = ExitAction.Revert;
|
||||||
|
if (Ui != null)
|
||||||
|
await Ui.DeactivateFieldAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum ExitAction { None, Confirm, Cancel, Revert }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,14 @@
|
|||||||
{
|
{
|
||||||
<span class="ec-field-value" @onclick:stopPropagation="true">
|
<span class="ec-field-value" @onclick:stopPropagation="true">
|
||||||
<input type="text" class="form-control form-control-sm" value="@(_editValue ?? DisplayValue)" @oninput="OnValueInput" @onfocusout="OnInputBlur" />
|
<input type="text" class="form-control form-control-sm" value="@(_editValue ?? DisplayValue)" @oninput="OnValueInput" @onfocusout="OnInputBlur" />
|
||||||
<OptionStateBadge AnchorId="@(ReadonlyBadgeAnchorId ?? "")" Source="@Source" OptionName="@Label" Value="@(RawValue ?? DisplayValue)" IsInteractive="@(DisplayMode == EffectiveConfigDisplayMode.Edit && !string.IsNullOrEmpty(ReadonlyBadgeAnchorId))" ShowEditableBadge="@ShowEditableBadge" HasPendingChange="@HasPendingChange" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="true" OnCancelRequested="@OnCancelRequested" />
|
<OptionStateBadge AnchorId="@(ReadonlyBadgeAnchorId ?? "")" Source="@Source" OptionName="@Label" Value="@(RawValue ?? DisplayValue)" IsInteractive="@(DisplayMode == EffectiveConfigDisplayMode.Edit && !string.IsNullOrEmpty(ReadonlyBadgeAnchorId))" ShowEditableBadge="@ShowEditableBadge" HasPendingChange="@HasPendingChange" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="true" OnConfirmRequested="@OnConfirmRequested" OnCancelRequested="@OnCancelRequested" OnRevertRequested="@OnRevertRequested" />
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="ec-field-value">
|
<span class="ec-field-value">
|
||||||
@DisplayValue
|
@DisplayValue
|
||||||
<OptionStateBadge AnchorId="@(ReadonlyBadgeAnchorId ?? "")" Source="@Source" OptionName="@Label" Value="@(RawValue ?? DisplayValue)" IsInteractive="@(DisplayMode == EffectiveConfigDisplayMode.Edit && !string.IsNullOrEmpty(ReadonlyBadgeAnchorId))" ShowEditableBadge="@ShowEditableBadge" HasPendingChange="@HasPendingChange" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="false" OnCancelRequested="@OnCancelRequested" />
|
<OptionStateBadge AnchorId="@(ReadonlyBadgeAnchorId ?? "")" Source="@Source" OptionName="@Label" Value="@(RawValue ?? DisplayValue)" IsInteractive="@(DisplayMode == EffectiveConfigDisplayMode.Edit && !string.IsNullOrEmpty(ReadonlyBadgeAnchorId))" ShowEditableBadge="@ShowEditableBadge" HasPendingChange="@HasPendingChange" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="false" OnConfirmRequested="@OnConfirmRequested" OnCancelRequested="@OnCancelRequested" OnRevertRequested="@OnRevertRequested" />
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,8 +42,12 @@ else
|
|||||||
[Parameter] public EventCallback<string> DisplayValueChanged { get; set; }
|
[Parameter] public EventCallback<string> DisplayValueChanged { get; set; }
|
||||||
/// <summary>When Edit and not readonly: raised when the input loses focus; argument is the current value.</summary>
|
/// <summary>When Edit and not readonly: raised when the input loses focus; argument is the current value.</summary>
|
||||||
[Parameter] public EventCallback<string?> OnBlur { get; set; }
|
[Parameter] public EventCallback<string?> OnBlur { get; set; }
|
||||||
/// <summary>When user clicks Cancel while this field is the active editor; parent should clear draft and deactivate.</summary>
|
/// <summary>When user clicks OK; parent should commit and deactivate.</summary>
|
||||||
|
[Parameter] public EventCallback OnConfirmRequested { get; set; }
|
||||||
|
/// <summary>When user clicks Cancel; parent should clear draft and deactivate.</summary>
|
||||||
[Parameter] public EventCallback OnCancelRequested { get; set; }
|
[Parameter] public EventCallback OnCancelRequested { get; set; }
|
||||||
|
/// <summary>When user clicks Revert; parent should revert pending, clear draft, and deactivate.</summary>
|
||||||
|
[Parameter] public EventCallback OnRevertRequested { get; set; }
|
||||||
[CascadingParameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
|
[CascadingParameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
|
||||||
|
|
||||||
private string? _editValue;
|
private string? _editValue;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
var change = c;
|
var change = c;
|
||||||
<div class="list-group-item ec-save-modal-item d-flex align-items-center gap-3 py-3 px-0 border-0 border-bottom">
|
<div class="list-group-item ec-save-modal-item d-flex align-items-center gap-3 py-3 px-0 border-0 border-bottom">
|
||||||
<div class="ec-save-modal-item-body flex-grow-1 min-width-0">
|
<div class="ec-save-modal-item-body flex-grow-1 min-width-0">
|
||||||
<span class="ec-save-modal-option badge rounded-pill bg-light text-dark border me-2">@change.OptionName</span>
|
<StatusBadge Kind="StatusBadgeKind.NeutralPill" Text="@change.OptionName" Class="ec-save-modal-option me-2" />
|
||||||
<span class="ec-save-modal-diff text-body">
|
<span class="ec-save-modal-diff text-body">
|
||||||
<span class="text-muted text-decoration-line-through">@FormatValue(change.OldValue)</span>
|
<span class="text-muted text-decoration-line-through">@FormatValue(change.OldValue)</span>
|
||||||
<span class="text-muted mx-1" aria-hidden="true">→</span>
|
<span class="text-muted mx-1" aria-hidden="true">→</span>
|
||||||
|
|||||||
@@ -93,6 +93,7 @@
|
|||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_optionHelpControllerRef?.CancelCloseFromLabelEnter();
|
||||||
_optionHelpKey = args.HelpKey;
|
_optionHelpKey = args.HelpKey;
|
||||||
_optionHelpTitle = args.OptionLabel;
|
_optionHelpTitle = args.OptionLabel;
|
||||||
_optionHelpAnchorId = args.AnchorId;
|
_optionHelpAnchorId = args.AnchorId;
|
||||||
|
|||||||
+3
-9
@@ -232,20 +232,14 @@
|
|||||||
margin-right: 0.25rem;
|
margin-right: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Badge: vertically centered, inline with value so when it wraps it stays part of the value flow */
|
/* StatusBadge inside field rows: alignment; spacing comes from StatusBadge .status-badge-inline */
|
||||||
::deep .ec-field .badge,
|
::deep .ec-field .status-badge,
|
||||||
::deep .ec-field-value-item .badge {
|
::deep .ec-field-value-item .status-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Inline badge (inside value): gap from text; when it wraps to own line the margin gives a subtle indent */
|
|
||||||
::deep .ec-badge-inline {
|
|
||||||
margin-left: 0.35rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
::deep .ec-section-header {
|
::deep .ec-section-header {
|
||||||
padding-left: 0.75rem;
|
padding-left: 0.75rem;
|
||||||
|
|||||||
@@ -48,4 +48,10 @@
|
|||||||
{
|
{
|
||||||
ScheduleClose();
|
ScheduleClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Called when opening or switching help (e.g. user clicked another label); cancels any pending 300ms close so the timer from a previous label leave does not dismiss the new help.</summary>
|
||||||
|
public void CancelCloseFromLabelEnter()
|
||||||
|
{
|
||||||
|
CancelCloseTimer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,16 @@
|
|||||||
{
|
{
|
||||||
@if (IsInteractive && Ui != null)
|
@if (IsInteractive && Ui != null)
|
||||||
{
|
{
|
||||||
<button type="button" id="@AnchorId" class="badge bg-secondary ec-badge-inline border-0 ms-1" title="@(Source?.GetReadOnlyTooltip())"
|
<StatusBadge Kind="StatusBadgeKind.ReadOnly" Text="readonly" Clickable="true"
|
||||||
@onclick="OnReadonlyClick" @onmouseleave="OnReadonlyMouseLeave">
|
Id="@AnchorId" Title="@(Source?.GetReadOnlyTooltip())"
|
||||||
readonly
|
OnClick="OnReadonlyClick" OnMouseLeave="OnReadonlyMouseLeave"
|
||||||
</button>
|
Class="status-badge-inline" />
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="badge bg-secondary ec-badge-inline ms-1" title="@(Source?.GetReadOnlyTooltip())">readonly</span>
|
<StatusBadge Kind="StatusBadgeKind.ReadOnly" Text="readonly"
|
||||||
|
Title="@(Source?.GetReadOnlyTooltip())"
|
||||||
|
Class="status-badge-inline" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (ShowEditableBadge)
|
else if (ShowEditableBadge)
|
||||||
@@ -22,31 +24,36 @@ else if (ShowEditableBadge)
|
|||||||
{
|
{
|
||||||
@if (IsActiveEditor)
|
@if (IsActiveEditor)
|
||||||
{
|
{
|
||||||
<button type="button"
|
<span class="status-badge-inline">
|
||||||
class="badge ec-badge-inline border-0 ms-1 bg-secondary"
|
<StatusBadge Kind="StatusBadgeKind.ActionOk" Text="ok" Clickable="true"
|
||||||
@onmousedown:preventDefault
|
Title="Apply this field change" AriaLabel="Apply change"
|
||||||
@onclick="OnCancelClick"
|
PreventBlurOnMouseDown="true" OnClick="OnConfirmClick" />
|
||||||
@onclick:stopPropagation="true"
|
<StatusBadge Kind="StatusBadgeKind.ActionCancel" Text="cancel" Clickable="true"
|
||||||
title="Cancel and clear unsaved changes for this field"
|
Title="Discard in-progress edits for this field" AriaLabel="Cancel edit"
|
||||||
aria-label="Cancel">
|
PreventBlurOnMouseDown="true" OnClick="OnCancelClick" />
|
||||||
cancel
|
@if (HasPendingChange)
|
||||||
</button>
|
{
|
||||||
|
<StatusBadge Kind="StatusBadgeKind.ActionRevert" Text="revert" Clickable="true"
|
||||||
|
Title="Remove this field from pending changes" AriaLabel="Revert pending change"
|
||||||
|
PreventBlurOnMouseDown="true" OnClick="OnRevertClick" />
|
||||||
|
}
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<button type="button"
|
<StatusBadge Kind="@(HasPendingChange ? StatusBadgeKind.Pending : StatusBadgeKind.Editable)"
|
||||||
class="badge ec-badge-inline border-0 ms-1 @(HasPendingChange ? "bg-warning text-dark" : "bg-primary")"
|
Text="@(HasPendingChange ? "pending" : "editable")"
|
||||||
@onclick="OnEditableClick"
|
Clickable="true"
|
||||||
@onclick:stopPropagation="true"
|
Title="@(HasPendingChange ? "Pending change" : "Edit this field")"
|
||||||
title="@(HasPendingChange ? "Pending change" : "Edit this field")"
|
AriaLabel="Edit"
|
||||||
aria-label="Edit">
|
OnClick="OnEditableClick"
|
||||||
@(HasPendingChange ? "pending" : "editable")
|
Class="status-badge-inline" />
|
||||||
</button>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="badge bg-primary ec-badge-inline ms-1">editable</span>
|
<StatusBadge Kind="StatusBadgeKind.Editable" Text="editable"
|
||||||
|
Class="status-badge-inline" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,22 +62,23 @@ else if (ShowEditableBadge)
|
|||||||
[Parameter] public ConfigValueSource? Source { get; set; }
|
[Parameter] public ConfigValueSource? Source { get; set; }
|
||||||
[Parameter] public string OptionName { get; set; } = "";
|
[Parameter] public string OptionName { get; set; } = "";
|
||||||
[Parameter] public object? Value { get; set; }
|
[Parameter] public object? Value { get; set; }
|
||||||
/// <summary>When true, render readonly as button and raise click/mouseleave via context.</summary>
|
|
||||||
[Parameter] public bool IsInteractive { get; set; }
|
[Parameter] public bool IsInteractive { get; set; }
|
||||||
/// <summary>When true, show editable/pending badge for non-readonly fields.</summary>
|
|
||||||
[Parameter] public bool ShowEditableBadge { get; set; }
|
[Parameter] public bool ShowEditableBadge { get; set; }
|
||||||
/// <summary>When true, badge shows "pending" and warning style.</summary>
|
|
||||||
[Parameter] public bool HasPendingChange { get; set; }
|
[Parameter] public bool HasPendingChange { get; set; }
|
||||||
/// <summary>When true, editable badge is a button; otherwise a static span.</summary>
|
|
||||||
[Parameter] public bool EditableAsButton { get; set; } = true;
|
[Parameter] public bool EditableAsButton { get; set; } = true;
|
||||||
/// <summary>When true, this field is the active editor; show Cancel instead of Edit and invoke OnCancelRequested on click.</summary>
|
|
||||||
[Parameter] public bool IsActiveEditor { get; set; }
|
[Parameter] public bool IsActiveEditor { get; set; }
|
||||||
/// <summary>When user clicks Cancel (while IsActiveEditor). Invoke before deactivating so parent can clear draft.</summary>
|
[Parameter] public EventCallback OnConfirmRequested { get; set; }
|
||||||
[Parameter] public EventCallback OnCancelRequested { get; set; }
|
[Parameter] public EventCallback OnCancelRequested { get; set; }
|
||||||
/// <summary>Field key for activation via context when editable badge is clicked.</summary>
|
[Parameter] public EventCallback OnRevertRequested { get; set; }
|
||||||
[Parameter] public string? FieldKey { get; set; }
|
[Parameter] public string? FieldKey { get; set; }
|
||||||
[CascadingParameter] public EffectiveConfigUiContext? Ui { get; set; }
|
[CascadingParameter] public EffectiveConfigUiContext? Ui { get; set; }
|
||||||
|
|
||||||
|
private async Task OnConfirmClick()
|
||||||
|
{
|
||||||
|
if (OnConfirmRequested.HasDelegate)
|
||||||
|
await OnConfirmRequested.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task OnCancelClick()
|
private async Task OnCancelClick()
|
||||||
{
|
{
|
||||||
if (OnCancelRequested.HasDelegate)
|
if (OnCancelRequested.HasDelegate)
|
||||||
@@ -79,6 +87,12 @@ else if (ShowEditableBadge)
|
|||||||
await Ui.DeactivateFieldAsync();
|
await Ui.DeactivateFieldAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task OnRevertClick()
|
||||||
|
{
|
||||||
|
if (OnRevertRequested.HasDelegate)
|
||||||
|
await OnRevertRequested.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private Task OnEditableClick()
|
private Task OnEditableClick()
|
||||||
{
|
{
|
||||||
if (Ui == null || string.IsNullOrEmpty(FieldKey)) return Task.CompletedTask;
|
if (Ui == null || string.IsNullOrEmpty(FieldKey)) return Task.CompletedTask;
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
@Path
|
@Path
|
||||||
@if (IsEditable)
|
@if (IsEditable)
|
||||||
{
|
{
|
||||||
<span class="badge bg-primary ms-1">editable</span>
|
<StatusBadge Kind="StatusBadgeKind.Editable" Text="editable" Class="ms-1" />
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="badge bg-secondary ms-1"> read-only</span>
|
<StatusBadge Kind="StatusBadgeKind.ReadOnly" Text="read-only" Class="ms-1" />
|
||||||
}
|
}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="table-responsive-cards">
|
<div class="table-responsive-cards">
|
||||||
|
|||||||
@@ -38,11 +38,11 @@ else if (_status != null)
|
|||||||
<li>
|
<li>
|
||||||
@if (f.IsManaged)
|
@if (f.IsManaged)
|
||||||
{
|
{
|
||||||
<span class="badge bg-primary me-1">managed</span>
|
<StatusBadge Kind="StatusBadgeKind.Managed" Text="managed" Class="me-1" />
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="badge bg-secondary me-1">@(f.Source.ToString().ToLowerInvariant())</span>
|
<StatusBadge Kind="StatusBadgeKind.SourceMain" Text="@(f.Source.ToString().ToLowerInvariant())" Class="me-1" />
|
||||||
}
|
}
|
||||||
@f.Path
|
@f.Path
|
||||||
@if (exists == true) { <text> ✓</text> }
|
@if (exists == true) { <text> ✓</text> }
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
@namespace DnsmasqWebUI.Components.Shared
|
||||||
|
|
||||||
|
@if (Clickable)
|
||||||
|
{
|
||||||
|
<button type="button"
|
||||||
|
class="@CssClass"
|
||||||
|
title="@Title"
|
||||||
|
aria-label="@(AriaLabel ?? Text)"
|
||||||
|
disabled="@Disabled"
|
||||||
|
id="@(string.IsNullOrEmpty(Id) ? null : Id)"
|
||||||
|
@onclick="OnClick"
|
||||||
|
@onclick:stopPropagation="@StopPropagation"
|
||||||
|
@onmousedown:preventDefault="@PreventBlurOnMouseDown"
|
||||||
|
@onmouseleave="OnMouseLeave">
|
||||||
|
@ChildContent
|
||||||
|
@if (ChildContent == null && !string.IsNullOrWhiteSpace(IconClass))
|
||||||
|
{
|
||||||
|
<i class="@IconClass" aria-hidden="true"></i>
|
||||||
|
<span class="visually-hidden">@Text</span>
|
||||||
|
}
|
||||||
|
else if (ChildContent == null)
|
||||||
|
{
|
||||||
|
@Text
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="@CssClass" title="@Title" id="@(string.IsNullOrEmpty(Id) ? null : Id)">
|
||||||
|
@ChildContent
|
||||||
|
@if (ChildContent == null && !string.IsNullOrWhiteSpace(IconClass))
|
||||||
|
{
|
||||||
|
<i class="@IconClass" aria-hidden="true"></i>
|
||||||
|
<span class="visually-hidden">@Text</span>
|
||||||
|
}
|
||||||
|
else if (ChildContent == null)
|
||||||
|
{
|
||||||
|
@Text
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public StatusBadgeKind Kind { get; set; }
|
||||||
|
[Parameter] public string Text { get; set; } = "";
|
||||||
|
[Parameter] public bool Clickable { get; set; }
|
||||||
|
[Parameter] public bool Disabled { get; set; }
|
||||||
|
[Parameter] public string? Title { get; set; }
|
||||||
|
[Parameter] public string? AriaLabel { get; set; }
|
||||||
|
[Parameter] public string? Id { get; set; }
|
||||||
|
[Parameter] public string? IconClass { get; set; }
|
||||||
|
[Parameter] public bool StopPropagation { get; set; } = true;
|
||||||
|
[Parameter] public bool PreventBlurOnMouseDown { get; set; }
|
||||||
|
[Parameter] public EventCallback OnClick { get; set; }
|
||||||
|
[Parameter] public EventCallback OnMouseLeave { get; set; }
|
||||||
|
[Parameter] public string? Class { get; set; }
|
||||||
|
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||||
|
|
||||||
|
private string CssClass
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var kindClass = Kind switch
|
||||||
|
{
|
||||||
|
StatusBadgeKind.Managed => "bg-primary",
|
||||||
|
StatusBadgeKind.SourceMain => "bg-secondary",
|
||||||
|
StatusBadgeKind.SourceInclude => "bg-secondary",
|
||||||
|
StatusBadgeKind.Editable => "bg-primary",
|
||||||
|
StatusBadgeKind.ReadOnly => "bg-secondary",
|
||||||
|
StatusBadgeKind.Pending => "bg-warning text-dark",
|
||||||
|
StatusBadgeKind.ActionOk => "bg-success",
|
||||||
|
StatusBadgeKind.ActionCancel => "bg-secondary",
|
||||||
|
StatusBadgeKind.ActionRevert => "bg-danger",
|
||||||
|
StatusBadgeKind.NeutralPill => "bg-light text-dark border rounded-pill",
|
||||||
|
_ => "bg-secondary"
|
||||||
|
};
|
||||||
|
return $"badge status-badge {kindClass} {Class}".Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/* Shared badge: compact label size, alignment, no button border when clickable */
|
||||||
|
.status-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.2em 0.5em;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge + .status-badge {
|
||||||
|
margin-left: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge-inline {
|
||||||
|
margin-left: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clickable badges: pointer, hover lift + glow, active press, focus for a11y */
|
||||||
|
button.status-badge {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease-out, filter 0.2s ease-out, box-shadow 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.status-badge:hover:not(:disabled) {
|
||||||
|
transform: scale(1.06) translateY(-2px);
|
||||||
|
filter: brightness(0.94);
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.status-badge:active:not(:disabled) {
|
||||||
|
transform: scale(0.94) translateY(0);
|
||||||
|
filter: brightness(0.88);
|
||||||
|
box-shadow: 0 0 0 rgba(0, 0, 0, 0);
|
||||||
|
transition-duration: 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.status-badge:focus-visible {
|
||||||
|
outline: 2px solid currentColor;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.status-badge:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace DnsmasqWebUI.Components.Shared;
|
||||||
|
|
||||||
|
/// <summary>Semantic badge intent for <see cref="StatusBadge"/>; maps to consistent CSS and behavior.</summary>
|
||||||
|
public enum StatusBadgeKind
|
||||||
|
{
|
||||||
|
Managed,
|
||||||
|
SourceMain,
|
||||||
|
SourceInclude,
|
||||||
|
Editable,
|
||||||
|
ReadOnly,
|
||||||
|
Pending,
|
||||||
|
ActionOk,
|
||||||
|
ActionCancel,
|
||||||
|
ActionRevert,
|
||||||
|
NeutralPill
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
@using DnsmasqWebUI.Models.Client.Abstractions
|
@using DnsmasqWebUI.Models.Client.Abstractions
|
||||||
@using DnsmasqWebUI.Infrastructure.Services.Abstractions
|
@using DnsmasqWebUI.Infrastructure.Services.Abstractions
|
||||||
@using DnsmasqWebUI.Components
|
@using DnsmasqWebUI.Components
|
||||||
|
@using DnsmasqWebUI.Components.Shared
|
||||||
@using DnsmasqWebUI.Components.Modals
|
@using DnsmasqWebUI.Components.Modals
|
||||||
@using DnsmasqWebUI.Components.Dnsmasq
|
@using DnsmasqWebUI.Components.Dnsmasq
|
||||||
@using DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
@using DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
||||||
|
|||||||
Reference in New Issue
Block a user