mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-03 22:12:00 +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)
|
||||
</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>
|
||||
</li>
|
||||
}
|
||||
@@ -63,7 +63,7 @@ else
|
||||
<EffectiveConfigOptionItem Label="@Descriptor.OptionName" DisplayValue="@(displayValue ?? "(not set)")" Source="@Descriptor.GetSource()" LabelTooltip="@_labelTooltip" OptionHelpKey="@_optionHelpKey" OptionHelpAnchorId="@_anchorId"
|
||||
ShowEditableBadge="true" FieldKey="@FieldKey" IsActive="@IsActive"
|
||||
ReadonlyBadgeAnchorId="@_readonlyBadgeAnchorId" RawValue="@value" HasPendingChange="@_hasPendingChange"
|
||||
DisplayValueChanged="@HandleDefaultValueChanged" OnBlur="@HandleDefaultBlur" OnCancelRequested="@HandleCancelEdit" />
|
||||
DisplayValueChanged="@HandleDefaultValueChanged" OnBlur="@HandleDefaultBlur" OnConfirmRequested="@HandleConfirmEdit" OnCancelRequested="@HandleCancelEdit" OnRevertRequested="@HandleRevertEdit" />
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ else
|
||||
private object? _valueAtActivation;
|
||||
private object? _draftValue;
|
||||
private bool _wasActive;
|
||||
private bool _cancelRequested;
|
||||
private ExitAction _exitAction = ExitAction.None;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
@@ -100,11 +100,11 @@ else
|
||||
_hasPendingChange = pending != null;
|
||||
_effectiveValue = pending != null ? pending.NewValue : Descriptor.GetValue();
|
||||
if (!_wasActive && IsActive)
|
||||
_valueAtActivation = _effectiveValue;
|
||||
if (_wasActive && !IsActive && !_cancelRequested)
|
||||
_valueAtActivation = Descriptor.GetValue();
|
||||
if (_wasActive && !IsActive && _exitAction == ExitAction.None)
|
||||
_ = InvokeAsync(CommitIfChangedAsync);
|
||||
if (_wasActive && !IsActive)
|
||||
_cancelRequested = false;
|
||||
_exitAction = ExitAction.None;
|
||||
_wasActive = IsActive;
|
||||
}
|
||||
|
||||
@@ -146,12 +146,30 @@ else
|
||||
await CommitIfChangedAsync();
|
||||
}
|
||||
|
||||
private async Task HandleCancelEdit()
|
||||
private async Task HandleConfirmEdit()
|
||||
{
|
||||
_cancelRequested = true;
|
||||
_draftValue = null;
|
||||
await CommitIfChangedAsync();
|
||||
_exitAction = ExitAction.Confirm;
|
||||
if (Ui != null)
|
||||
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">
|
||||
<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>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="ec-field-value">
|
||||
@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>
|
||||
}
|
||||
|
||||
@@ -42,8 +42,12 @@ else
|
||||
[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>
|
||||
[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; }
|
||||
/// <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;
|
||||
|
||||
private string? _editValue;
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
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="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="text-muted text-decoration-line-through">@FormatValue(change.OldValue)</span>
|
||||
<span class="text-muted mx-1" aria-hidden="true">→</span>
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
await InvokeAsync(StateHasChanged);
|
||||
return;
|
||||
}
|
||||
_optionHelpControllerRef?.CancelCloseFromLabelEnter();
|
||||
_optionHelpKey = args.HelpKey;
|
||||
_optionHelpTitle = args.OptionLabel;
|
||||
_optionHelpAnchorId = args.AnchorId;
|
||||
|
||||
+3
-9
@@ -232,20 +232,14 @@
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
/* Badge: vertically centered, inline with value so when it wraps it stays part of the value flow */
|
||||
::deep .ec-field .badge,
|
||||
::deep .ec-field-value-item .badge {
|
||||
/* StatusBadge inside field rows: alignment; spacing comes from StatusBadge .status-badge-inline */
|
||||
::deep .ec-field .status-badge,
|
||||
::deep .ec-field-value-item .status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
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) {
|
||||
::deep .ec-section-header {
|
||||
padding-left: 0.75rem;
|
||||
|
||||
@@ -48,4 +48,10 @@
|
||||
{
|
||||
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)
|
||||
{
|
||||
<button type="button" id="@AnchorId" class="badge bg-secondary ec-badge-inline border-0 ms-1" title="@(Source?.GetReadOnlyTooltip())"
|
||||
@onclick="OnReadonlyClick" @onmouseleave="OnReadonlyMouseLeave">
|
||||
readonly
|
||||
</button>
|
||||
<StatusBadge Kind="StatusBadgeKind.ReadOnly" Text="readonly" Clickable="true"
|
||||
Id="@AnchorId" Title="@(Source?.GetReadOnlyTooltip())"
|
||||
OnClick="OnReadonlyClick" OnMouseLeave="OnReadonlyMouseLeave"
|
||||
Class="status-badge-inline" />
|
||||
}
|
||||
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)
|
||||
@@ -22,31 +24,36 @@ else if (ShowEditableBadge)
|
||||
{
|
||||
@if (IsActiveEditor)
|
||||
{
|
||||
<button type="button"
|
||||
class="badge ec-badge-inline border-0 ms-1 bg-secondary"
|
||||
@onmousedown:preventDefault
|
||||
@onclick="OnCancelClick"
|
||||
@onclick:stopPropagation="true"
|
||||
title="Cancel and clear unsaved changes for this field"
|
||||
aria-label="Cancel">
|
||||
cancel
|
||||
</button>
|
||||
<span class="status-badge-inline">
|
||||
<StatusBadge Kind="StatusBadgeKind.ActionOk" Text="ok" Clickable="true"
|
||||
Title="Apply this field change" AriaLabel="Apply change"
|
||||
PreventBlurOnMouseDown="true" OnClick="OnConfirmClick" />
|
||||
<StatusBadge Kind="StatusBadgeKind.ActionCancel" Text="cancel" Clickable="true"
|
||||
Title="Discard in-progress edits for this field" AriaLabel="Cancel edit"
|
||||
PreventBlurOnMouseDown="true" OnClick="OnCancelClick" />
|
||||
@if (HasPendingChange)
|
||||
{
|
||||
<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
|
||||
{
|
||||
<button type="button"
|
||||
class="badge ec-badge-inline border-0 ms-1 @(HasPendingChange ? "bg-warning text-dark" : "bg-primary")"
|
||||
@onclick="OnEditableClick"
|
||||
@onclick:stopPropagation="true"
|
||||
title="@(HasPendingChange ? "Pending change" : "Edit this field")"
|
||||
aria-label="Edit">
|
||||
@(HasPendingChange ? "pending" : "editable")
|
||||
</button>
|
||||
<StatusBadge Kind="@(HasPendingChange ? StatusBadgeKind.Pending : StatusBadgeKind.Editable)"
|
||||
Text="@(HasPendingChange ? "pending" : "editable")"
|
||||
Clickable="true"
|
||||
Title="@(HasPendingChange ? "Pending change" : "Edit this field")"
|
||||
AriaLabel="Edit"
|
||||
OnClick="OnEditableClick"
|
||||
Class="status-badge-inline" />
|
||||
}
|
||||
}
|
||||
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 string OptionName { 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; }
|
||||
/// <summary>When true, show editable/pending badge for non-readonly fields.</summary>
|
||||
[Parameter] public bool ShowEditableBadge { get; set; }
|
||||
/// <summary>When true, badge shows "pending" and warning style.</summary>
|
||||
[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;
|
||||
/// <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; }
|
||||
/// <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; }
|
||||
/// <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; }
|
||||
[CascadingParameter] public EffectiveConfigUiContext? Ui { get; set; }
|
||||
|
||||
private async Task OnConfirmClick()
|
||||
{
|
||||
if (OnConfirmRequested.HasDelegate)
|
||||
await OnConfirmRequested.InvokeAsync();
|
||||
}
|
||||
|
||||
private async Task OnCancelClick()
|
||||
{
|
||||
if (OnCancelRequested.HasDelegate)
|
||||
@@ -79,6 +87,12 @@ else if (ShowEditableBadge)
|
||||
await Ui.DeactivateFieldAsync();
|
||||
}
|
||||
|
||||
private async Task OnRevertClick()
|
||||
{
|
||||
if (OnRevertRequested.HasDelegate)
|
||||
await OnRevertRequested.InvokeAsync();
|
||||
}
|
||||
|
||||
private Task OnEditableClick()
|
||||
{
|
||||
if (Ui == null || string.IsNullOrEmpty(FieldKey)) return Task.CompletedTask;
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
@Path
|
||||
@if (IsEditable)
|
||||
{
|
||||
<span class="badge bg-primary ms-1">editable</span>
|
||||
<StatusBadge Kind="StatusBadgeKind.Editable" Text="editable" Class="ms-1" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary ms-1"> read-only</span>
|
||||
<StatusBadge Kind="StatusBadgeKind.ReadOnly" Text="read-only" Class="ms-1" />
|
||||
}
|
||||
</h3>
|
||||
<div class="table-responsive-cards">
|
||||
|
||||
@@ -38,11 +38,11 @@ else if (_status != null)
|
||||
<li>
|
||||
@if (f.IsManaged)
|
||||
{
|
||||
<span class="badge bg-primary me-1">managed</span>
|
||||
<StatusBadge Kind="StatusBadgeKind.Managed" Text="managed" Class="me-1" />
|
||||
}
|
||||
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
|
||||
@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.Infrastructure.Services.Abstractions
|
||||
@using DnsmasqWebUI.Components
|
||||
@using DnsmasqWebUI.Components.Shared
|
||||
@using DnsmasqWebUI.Components.Modals
|
||||
@using DnsmasqWebUI.Components.Dnsmasq
|
||||
@using DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
||||
|
||||
Reference in New Issue
Block a user