From 002e9a5db5d0ac5a8d9b9844c1b7fc4db42ed0ab Mon Sep 17 00:00:00 2001 From: Alex Hope-O'Connor Date: Tue, 7 Apr 2026 17:36:52 +1000 Subject: [PATCH] Polish multi-value editors and hosts inline editing. Align the domain custom editor with the default multi-value layout, keep item numbering in edit mode, and restore unaffected single-value editor styling. Refine hosts inline editing with safer blur handling, explicit cancel actions, and better behavior when blank edits are abandoned. --- .../EffectiveConfigSection.razor.css | 43 ++- .../DomainEffectiveConfigListEditor.razor | 320 +++++++++++------- .../DomainEffectiveConfigListEditor.razor.css | 109 ++++-- .../DomainMultiValueDisplay.razor | 36 +- .../Fields/EffectiveConfigFieldDisplay.razor | 30 +- .../EffectiveConfigMultiValueListEditor.razor | 80 ++--- ...ectiveConfigMultiValueListEditor.razor.css | 16 +- .../Fields/EffectiveConfigMultiValueRow.razor | 53 +-- src/DnsmasqWebUI/Components/Pages/Hosts.razor | 102 ++++-- .../Components/Pages/Hosts.razor.css | 17 +- .../wwwroot/js/modules/table-scroll-cue.js | 14 + 11 files changed, 500 insertions(+), 320 deletions(-) diff --git a/src/DnsmasqWebUI/Components/EffectiveConfig/Composition/EffectiveConfigSection.razor.css b/src/DnsmasqWebUI/Components/EffectiveConfig/Composition/EffectiveConfigSection.razor.css index 9d9c746..5332b80 100644 --- a/src/DnsmasqWebUI/Components/EffectiveConfig/Composition/EffectiveConfigSection.razor.css +++ b/src/DnsmasqWebUI/Components/EffectiveConfig/Composition/EffectiveConfigSection.razor.css @@ -172,6 +172,16 @@ margin-bottom: 0.35rem; } +/* Option label + field-level badges (editable / ok / cancel) on one row */ +::deep .ec-field-label-row { + display: inline-flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0.25rem 0.5rem; + flex: 0 1 auto; + min-width: 0; +} + ::deep .ec-field:last-child { margin-bottom: 0; } @@ -206,7 +216,7 @@ /* When a field is active: value + badges in one block so any click inside (checkbox, input, ok/cancel) doesn't deactivate */ ::deep .ec-field-value-when-editing { display: inline-flex; - flex-wrap: wrap; + flex-wrap: nowrap; align-items: center; gap: 0.25rem; min-width: 0; @@ -239,11 +249,6 @@ word-break: break-word; } -/* Row-level ok/cancel (and revert) for multi-value: right-align on the row below the list */ -::deep .ec-field .ec-field-value-multi + .status-badge-inline { - margin-left: auto; -} - ::deep .ec-field-value-list { margin: 0; padding-left: 1.25rem; @@ -262,18 +267,24 @@ margin-bottom: 0; } -/* Multivalue display mode: editable entry row (no list number; align with list content) */ -::deep .ec-field-value-list li.ec-multi-list-entry-point { - list-style: none; - margin-left: -1.25rem; - margin-top: 0.15rem; -} - ::deep .ec-field-value-list li.ec-multi-list-empty-hint { list-style: none; margin-left: -1.25rem; } +/* Shared with default and custom (e.g. domain) multi-value editors */ +::deep .ec-field-value-list li.ec-multi-list-add-item { + list-style: none; + margin-left: -1.25rem; + margin-top: 0.25rem; + padding-top: 0.25rem; + border-top: 1px dashed var(--bs-border-color, #dee2e6); +} + +::deep .ec-field-value-list li.ec-multi-list-add-item > .ec-multi-list-row-controls > .status-badge-inline { + margin-left: auto; +} + /* Long paths/URLs wrap; allow break anywhere so nothing is clipped */ ::deep .ec-field-value-item { display: inline; @@ -300,7 +311,11 @@ margin-left: auto; } -@media (max-width: 991.98px) { +@media (max-width: 575.98px) { + ::deep .ec-field-value-when-editing { + flex-wrap: wrap; + } + ::deep .ec-field-value-when-editing > .status-badge-inline { flex-basis: 100%; margin-left: 0; diff --git a/src/DnsmasqWebUI/Components/EffectiveConfig/CustomDisplays/DomainEffectiveConfigListEditor.razor b/src/DnsmasqWebUI/Components/EffectiveConfig/CustomDisplays/DomainEffectiveConfigListEditor.razor index f6c3e3f..e64ea88 100644 --- a/src/DnsmasqWebUI/Components/EffectiveConfig/CustomDisplays/DomainEffectiveConfigListEditor.razor +++ b/src/DnsmasqWebUI/Components/EffectiveConfig/CustomDisplays/DomainEffectiveConfigListEditor.razor @@ -6,8 +6,7 @@ @inject INotificationService NotificationService
- @* ul: avoid
    markers (scoped CSS does not inherit .ec-multi-list-add-item list-style rules from the generic multi editor). *@ -
      +
        @if (_displayItems.Count > 0) { @for (var i = 0; i < _displayItems.Count; i++) @@ -19,116 +18,152 @@ var rowValue = item.Value;
      1. - @if (_editingIndex == rowIndex && !itemReadonly) - { -
        -

        Sets the domain suffix dnsmasq uses: DHCP domain option, which FQDNs hosts may claim, and (with expand-hosts) how simple names are qualified in DNS.

        - -
        - - -
        - @if (_draftUseRaw) - { - - } - else - { -
        - - -
        Use # to take the search domain from resolv.conf.
        +
        + @if (_editingIndex == rowIndex && !itemReadonly) + { +
        +
        +
        + Editor mode + +
        +
        + + +
        -
        - - -
        - @if (_draftScopeSelect == "range") + @if (_draftUseRaw) + { + + } + else {
        - - -
        Single IP, CIDR, or start,end pair per dnsmasq docs.
        + + +
        +
        Use # to take the search domain from resolv.conf.
        + +
        -
        - - -
        - } - else if (_draftScopeSelect == "interface") - {
        - - + +
        + @if (_draftScopeSelect == "range") + { +
        + + +
        Single IP, CIDR, or start,end pair per dnsmasq docs.
        +
        +
        + + +
        + } + else if (_draftScopeSelect == "interface") + { +
        + + +
        + } + else if (_draftScopeSelect == "unconditional") + { +
        + + + +
        + } } - else if (_draftScopeSelect == "unconditional") - { -
        - - -
        - } - } -
        - - +
        + + +
        -
        - } - else - { -
        - @DisplayLine(rowValue) - @if (!string.Equals(DomainRuleFields.FormatSummary(rowValue), rowValue, StringComparison.Ordinal)) - { - @rowValue - } - - - @if (!itemReadonly) + } + else + { +
        + @DisplayLine(rowValue) + @if (!string.Equals(DomainRuleFields.FormatSummary(rowValue), rowValue, StringComparison.Ordinal)) { - + @rowValue } - -
        - } + + + @if (!itemReadonly) + { + + } + +
        + } +
      2. } } @@ -145,19 +180,37 @@ @if (_addingNew) {
        -

        Sets the domain suffix dnsmasq uses: DHCP domain option, which FQDNs hosts may claim, and (with expand-hosts) how simple names are qualified in DNS.

        - -
        - - +
        +
        + Editor mode + +
        +
        + + +
        @if (_draftUseRaw) { - + } else { @@ -166,6 +219,16 @@ +
        +
        Use # to take the search domain from resolv.conf.
        + +
        @@ -181,8 +244,9 @@
        +
        Single IP, CIDR, or start,end pair per dnsmasq docs.
        } else if (_draftScopeSelect == "unconditional") { -
        +
        +
        } } @@ -230,7 +301,7 @@ } } -
    +
@code { @@ -354,6 +425,12 @@ _draftUseRaw = useRaw; } + private void ShowDomainRuleEditorHelp() => NotificationService.ShowInfo(DomainRuleEditorHelpText); + + private void ShowLocalSuffixHelp() => NotificationService.ShowInfo(LocalSuffixHelpText); + + private void ShowHashDomainHelp() => NotificationService.ShowInfo(HashDomainHelpText); + private void OnDraftScopeAfterChange() { if (_draftScopeSelect == "unconditional") @@ -490,4 +567,13 @@ await OnValuesChanged.InvokeAsync(next); StateHasChanged(); } + + private const string DomainRuleEditorHelpText = + "domain= sets the suffix dnsmasq uses for DHCP and local DNS naming. Guided mode builds the rule from fields; Raw line mode lets you enter the exact dnsmasq config line directly."; + + private const string LocalSuffixHelpText = + "Appending ,local behaves like a matching --local rule for this domain, making dnsmasq answer locally instead of forwarding it upstream. This is uncommon for an unconditional domain= rule."; + + private const string HashDomainHelpText = + "Use # as the domain value to take the search domain from resolv.conf instead of hard-coding a suffix in dnsmasq."; } diff --git a/src/DnsmasqWebUI/Components/EffectiveConfig/CustomDisplays/DomainEffectiveConfigListEditor.razor.css b/src/DnsmasqWebUI/Components/EffectiveConfig/CustomDisplays/DomainEffectiveConfigListEditor.razor.css index 0afbd81..64ce759 100644 --- a/src/DnsmasqWebUI/Components/EffectiveConfig/CustomDisplays/DomainEffectiveConfigListEditor.razor.css +++ b/src/DnsmasqWebUI/Components/EffectiveConfig/CustomDisplays/DomainEffectiveConfigListEditor.razor.css @@ -7,32 +7,22 @@ background-color: var(--bs-body-bg, #fff); } -.ec-domain-rule-editor .ec-field-value-list, -.ec-domain-rule-list { - list-style: none; +/* Keep
    markers (decimal) like the default multi-value editor; do not reset list-style here. */ +.ec-domain-rule-editor .ec-field-value-list { margin-bottom: 0; - padding-left: 0; -} - -.ec-domain-rule-list > li { - list-style: none; -} - -.ec-domain-rule-intro { - font-size: 0.8rem; - color: var(--bs-secondary-color, #6c757d); - margin-bottom: 0.35rem; - line-height: 1.35; } .ec-domain-rule-row { - display: flex; - flex-direction: column; - gap: 0.5rem; min-height: 2rem; } -.ec-domain-rule-row.ec-domain-rule-row-editing { +.ec-domain-rule-row-body { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.ec-domain-rule-row.ec-domain-rule-row-editing .ec-domain-rule-row-body { align-items: stretch; } @@ -52,13 +42,77 @@ color: var(--bs-secondary-color, #6c757d); } +.ec-domain-rule-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem 0.75rem; + flex-wrap: wrap; +} + +.ec-domain-rule-toolbar__group { + display: inline-flex; + align-items: center; + gap: 0.35rem; +} + +.ec-domain-rule-mode-toggle { + display: inline-flex; + align-items: center; + gap: 0.2rem; + padding: 0.2rem; + border: 1px solid var(--bs-border-color, #dee2e6); + border-radius: 999px; + background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); +} + +.ec-domain-rule-mode-btn { + border: 0; + border-radius: 999px; + padding: 0.2rem 0.65rem; + font-size: 0.75rem; + font-weight: 600; + color: #475569; + background: transparent; + box-shadow: none; +} + +.ec-domain-rule-mode-btn:hover, +.ec-domain-rule-mode-btn:focus-visible { + color: #0f172a; + background: rgba(148, 163, 184, 0.12); +} + +.ec-domain-rule-mode-btn--active, +.ec-domain-rule-mode-btn--active:hover, +.ec-domain-rule-mode-btn--active:focus-visible { + color: #fff; + background: linear-gradient(180deg, #0d6efd 0%, #0a58ca 100%); +} + +.ec-domain-rule-help { + line-height: 1; + text-decoration: none; +} + +.ec-domain-rule-help:hover, +.ec-domain-rule-help:focus-visible { + color: var(--bs-primary) !important; +} + .ec-domain-rule-actions { display: flex; align-items: center; + justify-content: flex-end; flex-wrap: wrap; gap: 0.35rem; } +.ec-domain-rule-editor .ec-field-value-list li.ec-multi-list-add-item .ec-domain-rule-actions .status-badge-inline, +.ec-domain-rule-actions .status-badge-inline { + margin-left: 0; +} + .ec-domain-rule-summary { display: flex; align-items: center; @@ -77,3 +131,20 @@ color: var(--bs-secondary-color, #6c757d); margin-top: -0.25rem; } + +.ec-domain-rule-hint-row { + display: flex; + align-items: flex-start; + gap: 0.25rem; +} + +.ec-domain-rule-local-check { + display: flex; + align-items: flex-start; + gap: 0.35rem; +} + +.ec-domain-rule-local-check .form-check-label { + flex: 1 1 auto; + min-width: 0; +} diff --git a/src/DnsmasqWebUI/Components/EffectiveConfig/CustomDisplays/DomainMultiValueDisplay.razor b/src/DnsmasqWebUI/Components/EffectiveConfig/CustomDisplays/DomainMultiValueDisplay.razor index 3b3f63f..4bf3ceb 100644 --- a/src/DnsmasqWebUI/Components/EffectiveConfig/CustomDisplays/DomainMultiValueDisplay.razor +++ b/src/DnsmasqWebUI/Components/EffectiveConfig/CustomDisplays/DomainMultiValueDisplay.razor @@ -38,42 +38,10 @@ else } } - @if (Context.ShowEditableBadge && Context.EffectiveItems.Count > 0) - { -
  1. - -
  2. - } - else if (Context.EffectiveItems.Count == 0) + else {
  3. - @if (Context.ShowEditableBadge) - { - (none) - - } - else - { - (none) - } + (none)
  4. }
diff --git a/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigFieldDisplay.razor b/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigFieldDisplay.razor index da50663..9ff2faa 100644 --- a/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigFieldDisplay.razor +++ b/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigFieldDisplay.razor @@ -49,22 +49,22 @@ else if (Descriptor.IsMultiValue) { @* Custom multi: row badge only while this field is active (ok/cancel/revert). Otherwise per-item / entry badges in the fragment suffice — avoids duplicating "readonly" when RowSource matches item sources. *@ var customMultiActiveEditor = IsEditMode && IsActive && !Descriptor.IsCapabilityDisabled; -
  • - @Descriptor.OptionName: - - - - @customMulti(_multiContext) - +
  • +
    + @Descriptor.OptionName: + @if (!Descriptor.IsCapabilityDisabled) + { + + } +
    + + + @customMulti(_multiContext) - - @if (customMultiActiveEditor) - { - - } +
  • } else if (_multiContext is not null) diff --git a/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigMultiValueListEditor.razor b/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigMultiValueListEditor.razor index e10878f..e843c8c 100644 --- a/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigMultiValueListEditor.razor +++ b/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigMultiValueListEditor.razor @@ -19,47 +19,49 @@ var rowValue = item.Value;
  • - @if (_editingIndex == rowIndex && !itemReadonly) - { -
    -
    - +
    + @if (_editingIndex == rowIndex && !itemReadonly) + { +
    +
    + +
    + + + +
    - - - - -
    - } - else - { - @rowValue + } + else + { + @rowValue - - - @if (!itemReadonly) - { - - } - - } + + + @if (!itemReadonly) + { + + } + + } +
  • } } diff --git a/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigMultiValueListEditor.razor.css b/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigMultiValueListEditor.razor.css index 6e4c8f2..a518b10 100644 --- a/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigMultiValueListEditor.razor.css +++ b/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigMultiValueListEditor.razor.css @@ -13,14 +13,18 @@ } .ec-multi-list-row { + min-height: 2rem; +} + +.ec-multi-list-row-body { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; - min-height: 2rem; + min-width: 0; } -.ec-multi-list-row.ec-multi-list-row-with-controls { +.ec-multi-list-row.ec-multi-list-row-with-controls .ec-multi-list-row-body { align-items: flex-start; } @@ -47,18 +51,18 @@ width: 100%; } -.ec-multi-list-row .ec-field-value-item { +.ec-multi-list-row-body .ec-field-value-item { min-width: 0; } -.ec-multi-list-row .status-badge-inline { +.ec-multi-list-row-body .status-badge-inline { display: inline-flex; align-items: center; gap: 0.25rem; flex-shrink: 0; } -.ec-multi-list-row .form-control.ec-multi-list-input { +.ec-multi-list-row-body .form-control.ec-multi-list-input { max-width: none; flex-shrink: 0; width: 100%; @@ -86,7 +90,7 @@ max-width: none; } -.ec-multi-list-add-item .status-badge-inline { +.ec-multi-list-add-item > .ec-multi-list-row-controls > .status-badge-inline { margin-left: auto; } diff --git a/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigMultiValueRow.razor b/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigMultiValueRow.razor index fac8ec6..6748c7e 100644 --- a/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigMultiValueRow.razor +++ b/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigMultiValueRow.razor @@ -2,8 +2,18 @@ @using DnsmasqWebUI.Models.Dnsmasq @using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig -
  • - @Context.Descriptor.OptionName: +
  • +
    + @Context.Descriptor.OptionName: + @if (Context.IsActiveEditor || Context.ShowEditableBadge) + { + + } +
    @if (Context.IsActiveEditor) { - } else { @@ -44,42 +49,10 @@
  • } } - @if (Context.ShowEditableBadge && Context.EffectiveItems.Count > 0) - { -
  • - -
  • - } else if (Context.EffectiveItems.Count == 0) {
  • - @if (Context.ShowEditableBadge) - { - (none) - - } - else - { - (none) - } + (none)
  • } diff --git a/src/DnsmasqWebUI/Components/Pages/Hosts.razor b/src/DnsmasqWebUI/Components/Pages/Hosts.razor index 472e3de..9ae45fd 100644 --- a/src/DnsmasqWebUI/Components/Pages/Hosts.razor +++ b/src/DnsmasqWebUI/Components/Pages/Hosts.razor @@ -230,6 +230,12 @@ else @onclick="OnManagedHeaderSaveAsync"> Save + } else if (ManagedSelectionCount == 1) { @@ -865,6 +871,11 @@ else await TryFinishHostEditFromKeyboardAsync(); } + private async Task OnManagedHeaderCancelAsync() + { + await DiscardCurrentEditAsync(); + } + private async Task OnManagedHeaderEditAsync() { DeactivateEffectiveConfigFieldIfActive(); @@ -1057,6 +1068,11 @@ else private void RevealHostFieldValidation() => _showHostFieldValidation = true; + private bool IsCurrentEditBlank => + string.IsNullOrWhiteSpace(_editAddress) + && ParseNamesList(_editNames).Count == 0 + && string.IsNullOrWhiteSpace(_editInlineComment); + private void DeactivateEffectiveConfigFieldIfActive() { if (Session.Ui.ActiveFieldKey != null) @@ -1072,41 +1088,8 @@ else private async Task OnInvalidEditDiscardAsync() { - if (string.IsNullOrEmpty(_editingRowId) || _status == null || string.IsNullOrWhiteSpace(_status.ManagedHostsFilePath)) - { - _showInvalidHostEditModal = false; - CancelEdit(); - await InvokeAsync(StateHasChanged); - return; - } - - var list = CloneEntries(CurrentManagedEntries).ToList(); - var entry = list.FirstOrDefault(MatchesEditingId); - var wasNew = entry != null && entry.Id.StartsWith("new:", StringComparison.Ordinal); - if (wasNew) - { - list.Remove(entry!); - Session.Draft.SetManagedHostsDraft( - CloneEntries(_baselineManagedEntries), - list, - _status.ManagedHostsFilePath); - RebuildManagedOverlay(); - InvalidateVisibleGroupsCache(); - } - _showInvalidHostEditModal = false; - var prevId = _editingRowId; - CancelEdit(); - if (wasNew) - _selectedManagedRowIds.Clear(); - else - { - _selectedManagedRowIds.Clear(); - if (!string.IsNullOrEmpty(prevId)) - _selectedManagedRowIds.Add(prevId); - } - - await InvokeAsync(StateHasChanged); + await DiscardCurrentEditAsync(); } private async Task OnEditFieldKeyDown(KeyboardEventArgs e) @@ -1131,6 +1114,12 @@ else if (_showInvalidHostEditModal || string.IsNullOrEmpty(_editingRowId)) return; + if (IsCurrentEditBlank) + { + await DiscardCurrentEditAsync(); + return; + } + if (IsCurrentEditValid) await CommitAndEndInlineEditAsync(); else @@ -1168,6 +1157,10 @@ else { try { + var recentCueInteraction = await _hostsTableScrollCueModule.InvokeAsync("wasRecentCueInteraction"); + if (recentCueInteraction) + return; + outside = await _hostsTableScrollCueModule.InvokeAsync("isFocusOutsideElement", _editingRowRef); } catch (Exception ex) when (ex is JSDisconnectedException or JSException or ObjectDisposedException or TaskCanceledException) @@ -1178,6 +1171,12 @@ else if (outside) { + if (IsCurrentEditBlank) + { + await DiscardCurrentEditAsync(); + return; + } + if (IsCurrentEditValid) await CommitAndEndInlineEditAsync(); else @@ -1220,6 +1219,41 @@ else await InvokeAsync(StateHasChanged); } + private async Task DiscardCurrentEditAsync() + { + if (string.IsNullOrEmpty(_editingRowId) || _status == null || string.IsNullOrWhiteSpace(_status.ManagedHostsFilePath)) + { + CancelEdit(); + await InvokeAsync(StateHasChanged); + return; + } + + var list = CloneEntries(CurrentManagedEntries).ToList(); + var entry = list.FirstOrDefault(MatchesEditingId); + var wasNew = entry != null && entry.Id.StartsWith("new:", StringComparison.Ordinal); + var prevId = _editingRowId; + + if (wasNew) + { + list.Remove(entry!); + Session.Draft.SetManagedHostsDraft( + CloneEntries(_baselineManagedEntries), + list, + _status.ManagedHostsFilePath); + RebuildManagedOverlay(); + InvalidateVisibleGroupsCache(); + } + + CancelEdit(); + _showInvalidHostEditModal = false; + _selectedManagedRowIds.Clear(); + if (!wasNew && !string.IsNullOrEmpty(prevId)) + _selectedManagedRowIds.Add(prevId); + _selectionAnchorRowId = !wasNew ? prevId : null; + + await InvokeAsync(StateHasChanged); + } + private bool MatchesEditingId(HostEntry e) { var id = string.IsNullOrEmpty(e.Id) ? $"managed:{e.LineNumber}" : e.Id; diff --git a/src/DnsmasqWebUI/Components/Pages/Hosts.razor.css b/src/DnsmasqWebUI/Components/Pages/Hosts.razor.css index 4042433..c491e79 100644 --- a/src/DnsmasqWebUI/Components/Pages/Hosts.razor.css +++ b/src/DnsmasqWebUI/Components/Pages/Hosts.razor.css @@ -376,7 +376,7 @@ /* Fixed column for actions avoids path/subtitle reflow when buttons show or hide. */ .hosts-group-header__path-row--with-managed-actions { display: grid; - grid-template-columns: minmax(0, 1fr) var(--hosts-managed-actions-col, 9.75rem); + grid-template-columns: minmax(0, 1fr) var(--hosts-managed-actions-col, 13.5rem); align-items: center; column-gap: 0.5rem; } @@ -477,6 +477,19 @@ border-color: rgba(8, 66, 152, 0.92); } +.hosts-managed-action-btn--cancel { + color: #334155; + background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); + border-color: rgba(148, 163, 184, 0.38); +} + +.hosts-managed-action-btn--cancel:hover, +.hosts-managed-action-btn--cancel:focus-visible { + color: #0f172a; + background: linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%); + border-color: rgba(100, 116, 139, 0.42); +} + .hosts-managed-action-btn--edit { color: #0f172a; background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); @@ -589,7 +602,7 @@ @media (max-width: 640.98px) { .hosts-group-header__path-row--with-managed-actions { - --hosts-managed-actions-col: 9rem; + --hosts-managed-actions-col: 12rem; } .hosts-group-header__managed-actions { diff --git a/src/DnsmasqWebUI/wwwroot/js/modules/table-scroll-cue.js b/src/DnsmasqWebUI/wwwroot/js/modules/table-scroll-cue.js index 4f20c98..5d6230c 100644 --- a/src/DnsmasqWebUI/wwwroot/js/modules/table-scroll-cue.js +++ b/src/DnsmasqWebUI/wwwroot/js/modules/table-scroll-cue.js @@ -10,10 +10,16 @@ const POLL_MS = 100; const BTN_LEFT_CLASS = 'app-table-cue-btn--left'; const BTN_RIGHT_CLASS = 'app-table-cue-btn--right'; const CUE_BTN_CLASS = 'app-table-cue-btn'; +const RECENT_CUE_INTERACTION_MS = 300; let pollId = null; let resizeListener = null; const scrollListeners = new WeakMap(); +let lastCueInteractionAt = 0; + +function markCueInteraction() { + lastCueInteractionAt = Date.now(); +} function applyScrollCue(scrollEl, hostEl) { if (!scrollEl || !hostEl) return; @@ -58,7 +64,9 @@ function ensureCueButtons(hostEl) { leftBtn.type = 'button'; leftBtn.className = CUE_BTN_CLASS + ' ' + BTN_LEFT_CLASS; leftBtn.setAttribute('aria-label', 'Scroll table left'); + leftBtn.addEventListener('pointerdown', markCueInteraction); leftBtn.addEventListener('click', function () { + markCueInteraction(); scrollToSide(hostEl.querySelector(WRAPPER_SELECTOR), 'left'); }); hostEl.appendChild(leftBtn); @@ -69,7 +77,9 @@ function ensureCueButtons(hostEl) { rightBtn.type = 'button'; rightBtn.className = CUE_BTN_CLASS + ' ' + BTN_RIGHT_CLASS; rightBtn.setAttribute('aria-label', 'Scroll table right'); + rightBtn.addEventListener('pointerdown', markCueInteraction); rightBtn.addEventListener('click', function () { + markCueInteraction(); scrollToSide(hostEl.querySelector(WRAPPER_SELECTOR), 'right'); }); hostEl.appendChild(rightBtn); @@ -170,3 +180,7 @@ export function isFocusOutsideElement(container) { if (!active) return true; return !container.contains(active); } + +export function wasRecentCueInteraction() { + return Date.now() - lastCueInteractionAt <= RECENT_CUE_INTERACTION_MS; +}