fix(#4713): dark-mode disabled badge contrast and active-first plugin sort

This commit is contained in:
Rod Boev 2026-06-24 09:04:16 -04:00 committed by nesquena-hermes
parent 98e13bc92a
commit 7c1cd99961
3 changed files with 119 additions and 1 deletions

View file

@ -8253,6 +8253,25 @@ async function handlePluginEnableToggle(pluginKey, checked){
}
}
function _pluginActivationState(plugin){
const activation=(plugin&&typeof plugin.activation==='string')
? plugin.activation
: (plugin&&plugin.enabled===false ? 'disabled' : 'enabled');
if(activation==='exclusive'||activation==='provider') return 'provider';
if(activation==='enabled') return 'enabled';
return 'disabled';
}
function _partitionPluginsActiveFirst(plugins){
const active=[];
const inactive=[];
for(const p of plugins){
if(_pluginActivationState(p)==='disabled') inactive.push(p);
else active.push(p);
}
return active.concat(inactive);
}
async function loadPluginsPanel(){
const list=$('pluginsList');
const empty=$('pluginsEmpty');
@ -8271,7 +8290,7 @@ async function loadPluginsPanel(){
}
if(empty) empty.style.display='none';
list.style.display='';
for(const plugin of plugins){
for(const plugin of _partitionPluginsActiveFirst(plugins)){
list.appendChild(_buildPluginCard(plugin));
}
}catch(e){

View file

@ -4654,6 +4654,7 @@ main.main > #mainPlugin{display:none;}
.plugin-card-header{cursor:default;}
.plugin-card-header:hover{background:transparent;}
.plugin-card-badge-disabled{background:var(--surface);color:var(--muted);}
:root.dark .provider-card-badge.plugin-card-badge-disabled{background:var(--surface);color:var(--muted);}
.plugin-hook-list{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px;}
.plugin-hook-badge{display:inline-flex;align-items:center;border:1px solid var(--border2);background:var(--code-bg);color:var(--text);border-radius:999px;padding:3px 8px;font-size:11px;font-family:var(--font-mono);}
.plugin-hook-empty{font-size:12px;color:var(--muted);font-style:italic;}

View file

@ -0,0 +1,98 @@
"""Tests for issue #4713: dark-mode disabled badge color and active-first plugin sort."""
import json
import os
import re
import subprocess
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STYLE_CSS = os.path.join(REPO_ROOT, 'static', 'style.css')
PANELS_JS = os.path.join(REPO_ROOT, 'static', 'panels.js')
def test_css_dark_mode_disabled_badge_compound_rule():
"""style.css must contain a compound selector that applies var(--muted) to
disabled badges in dark mode at specificity (0,4,0)."""
with open(STYLE_CSS, encoding='utf-8') as f:
css = f.read()
# Must have :root.dark combined with .provider-card-badge and .plugin-card-badge-disabled
pattern = r':root\.dark\s+\.provider-card-badge\.plugin-card-badge-disabled\b[^}]*color\s*:\s*var\(--muted\)'
assert re.search(pattern, css), (
'style.css is missing a compound :root.dark .provider-card-badge.plugin-card-badge-disabled '
'rule with color:var(--muted)'
)
def test_panels_js_render_loop_uses_partition():
"""panels.js loadPluginsPanel render loop must call _partitionPluginsActiveFirst."""
with open(PANELS_JS, encoding='utf-8') as f:
js = f.read()
assert '_partitionPluginsActiveFirst' in js, (
'panels.js is missing _partitionPluginsActiveFirst'
)
# The for-of loop inside loadPluginsPanel must use the partition helper
assert 'for(const plugin of _partitionPluginsActiveFirst(plugins))' in js or \
'for (const plugin of _partitionPluginsActiveFirst(plugins))' in js, (
'loadPluginsPanel render loop does not call _partitionPluginsActiveFirst(plugins)'
)
def _extract_helpers(js_source):
"""Extract the two helper functions from panels.js source."""
start = js_source.find('function _pluginActivationState(')
assert start != -1, '_pluginActivationState not found in panels.js'
end = js_source.find('\nasync function loadPluginsPanel(', start)
assert end != -1, 'Could not find end boundary for helpers'
return js_source[start:end].strip()
def _run_node(script):
result = subprocess.run(
['node', '-e', script],
capture_output=True, text=True, timeout=15
)
assert result.returncode == 0, f'node exited {result.returncode}: {result.stderr}'
return json.loads(result.stdout.strip())
def test_partition_stability():
"""_partitionPluginsActiveFirst preserves relative order within each bucket."""
with open(PANELS_JS, encoding='utf-8') as f:
js = f.read()
helpers = _extract_helpers(js)
script = helpers + r"""
const input = [
{activation:'enabled', key:'a'},
{activation:'disabled', key:'b'},
{activation:'provider', key:'c'},
{activation:'disabled', key:'d'},
{activation:'enabled', key:'e'}
];
const copy = JSON.parse(JSON.stringify(input));
const result = _partitionPluginsActiveFirst(input);
console.log(JSON.stringify({result: result.map(p=>p.key)}));
"""
data = _run_node(script)
assert data['result'] == ['a', 'c', 'e', 'b', 'd'], (
f'Expected [a,c,e,b,d] but got {data["result"]}'
)
def test_partition_input_immutability():
"""_partitionPluginsActiveFirst must not mutate the input array."""
with open(PANELS_JS, encoding='utf-8') as f:
js = f.read()
helpers = _extract_helpers(js)
script = helpers + r"""
const input = [
{activation:'enabled', key:'a'},
{activation:'disabled', key:'b'},
{activation:'provider', key:'c'},
{activation:'disabled', key:'d'}
];
const copy = JSON.parse(JSON.stringify(input));
_partitionPluginsActiveFirst(input);
console.log(JSON.stringify({inputUnchanged: JSON.stringify(input) === JSON.stringify(copy)}));
"""
data = _run_node(script)
assert data['inputUnchanged'] is True, 'Input array was mutated by _partitionPluginsActiveFirst'