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:
2026-02-28 21:34:12 +10:00
parent 71a8a61cb3
commit f8754d8d6e
13 changed files with 239 additions and 56 deletions
@@ -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;
@@ -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