From 05f08fb6280a2dca0e7470bfc029042e94e481aa Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Wed, 16 Sep 2026 15:03:47 +0800 Subject: [PATCH] style: tuck brain filters into a compact menu --- Makefile | 2 +- VERSION | 2 +- assets/dashboard.css | 19 ++++++++++++++----- assets/dashboard.html | 2 +- assets/dashboard.js | 13 ++++++++++++- scripts/test-dashboard-brain-menu.mjs | 26 ++++++++++++++++++++++++++ 6 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 scripts/test-dashboard-brain-menu.mjs diff --git a/Makefile b/Makefile index 160a02b..56cee6b 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ build: ## Build the devbrain binary at the repo root (version from VERSION) test: ## Go vet + full test suite, plus dashboard regression when Node is available @scripts/test-guard.sh $(GOTESTFLAGS) - @if command -v node >/dev/null 2>&1; then node scripts/test-dashboard-concurrency.mjs; else echo "dashboard regression skipped (node unavailable)"; fi + @if command -v node >/dev/null 2>&1; then node scripts/test-dashboard-concurrency.mjs && node scripts/test-dashboard-brain-menu.mjs; else echo "dashboard regression skipped (node unavailable)"; fi release: ## Manual fallback — CI releases on tag push (.github/workflows/release.yml) GITHUB_TOKEN=$${GITHUB_TOKEN:-$$(gh auth token)} sh -c '\ diff --git a/VERSION b/VERSION index 5b5dc42..f93a854 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.5.26 +1.5.27 diff --git a/assets/dashboard.css b/assets/dashboard.css index 32850d9..5a1aaae 100644 --- a/assets/dashboard.css +++ b/assets/dashboard.css @@ -402,10 +402,19 @@ body.dragsel #moon{transform:scale(1.1)} /* moon grows during a drag } [hidden]{display:none!important} -.brain-controls,#brain-buttons{display:flex;align-items:center;gap:5px;flex-wrap:wrap} -.brain-controls>span{color:var(--muted);font-size:11px} -#brain-buttons button{font-size:12px;padding:5px 9px;color:var(--muted)} -#brain-buttons button[aria-pressed=true]{color:var(--text);border-color:var(--accent);background:rgba(10,132,255,.18)} +.brain-controls{position:relative;flex:none} +.brain-controls>summary{display:flex;align-items:center;gap:6px;padding:6px 8px;min-height:32px;list-style:none;cursor:pointer;color:var(--muted);font-size:12px;border-radius:6px} +.brain-controls>summary::-webkit-details-marker{display:none} +.brain-controls>summary::after{content:"";width:5px;height:5px;border-right:1px solid;border-bottom:1px solid;transform:translateY(-2px) rotate(45deg)} +.brain-controls>summary:hover,.brain-controls[open]>summary{background:var(--panel);color:var(--text)} +#brain-count:empty{display:none} +#brain-count{font-variant-numeric:tabular-nums;color:var(--text)} +#brain-buttons{position:absolute;top:calc(100% + 6px);right:0;min-width:180px;max-width:calc(100vw - 32px);padding:4px;background:var(--panel);border:1px solid var(--line);border-radius:8px;box-shadow:0 6px 18px rgba(0,0,0,.3)} +#brain-buttons button{display:flex;align-items:center;gap:9px;width:100%;padding:8px;text-align:left;font-size:13px;background:transparent;border:0;border-radius:4px;overflow-wrap:anywhere} +#brain-buttons button:hover{background:var(--line)} +#brain-buttons button:disabled{opacity:.5;cursor:not-allowed} +.brain-check{display:inline-flex;align-items:center;justify-content:center;flex:none;width:15px;height:15px;border:1px solid var(--muted);border-radius:3px;font-size:11px} +[aria-pressed=true]>.brain-check{background:var(--accent);border-color:var(--accent);color:var(--on-accent)} #brain-error{padding:12px 16px;border-bottom:2px solid var(--accent);background:var(--panel);font-weight:600} #pf-prefs-brain{max-width:160px;font-size:11px;padding:3px 6px} -@media(max-width:640px){.brain-controls{width:100%;order:5}#pf-prefs-brain{max-width:110px}} +@media(max-width:640px){.brain-controls>summary,#brain-buttons button{min-height:44px}#pf-prefs-brain{max-width:110px}} diff --git a/assets/dashboard.html b/assets/dashboard.html index b98e683..f726fc0 100644 --- a/assets/dashboard.html +++ b/assets/dashboard.html @@ -9,7 +9,7 @@ - + diff --git a/assets/dashboard.js b/assets/dashboard.js index 4477ea0..3d20a44 100644 --- a/assets/dashboard.js +++ b/assets/dashboard.js @@ -20,8 +20,13 @@ function brainNotice(message=''){ $('#brain-error').textContent=text; $('#brain-error').hidden=!text; } function renderBrainButtons(){ + const focused=$('#brain-buttons').contains(document.activeElement)?document.activeElement.dataset.brain:null; $('#brain-controls').hidden=BRAINS.length<2; - $('#brain-buttons').innerHTML=BRAINS.map(b=>``).join(''); + const count=selectedBrains().length; + $('#brain-count').textContent=count===BRAINS.length?'':`${count}/${BRAINS.length}`; + $('#brain-controls summary').title=count===BRAINS.length?'All Brains Shown':`${count} of ${BRAINS.length} Brains Shown`; + $('#brain-buttons').innerHTML=BRAINS.map(b=>``).join(''); + if(focused) [...$('#brain-buttons').querySelectorAll('button')].find(b=>b.dataset.brain===focused)?.focus(); $('#brain-buttons').querySelectorAll('button').forEach(b=>b.onclick=()=>{ if(window.preferencesEditing) return; if(HIDDEN_BRAINS.has(b.dataset.brain)) HIDDEN_BRAINS.delete(b.dataset.brain); else HIDDEN_BRAINS.add(b.dataset.brain); @@ -31,6 +36,12 @@ function renderBrainButtons(){ DATA.tasks=DATA.tasks.filter(brainIncluded); NS.runs=NS.runs.filter(brainIncluded); render(); if(VIEW==='monitor') renderMonitor(); load(); checkNS(); }); } +document.addEventListener('click',e=>{if(!e.composedPath().includes($('#brain-controls'))) $('#brain-controls').open=false;}); +document.addEventListener('keydown',e=>{ + if(e.key==='Escape' && $('#brain-controls').open){ + $('#brain-controls').open=false; $('#brain-controls summary').focus(); + } +}); const BRAINS_READY=(async()=>{ const response=await fetch('/api/brains'); if(!response.ok) throw new Error('Could not load brains'); BRAINS=(await response.json()).brains; renderBrainButtons(); brainNotice(); diff --git a/scripts/test-dashboard-brain-menu.mjs b/scripts/test-dashboard-brain-menu.mjs new file mode 100644 index 0000000..15db9a9 --- /dev/null +++ b/scripts/test-dashboard-brain-menu.mjs @@ -0,0 +1,26 @@ +import assert from 'node:assert/strict'; +import {readFileSync} from 'node:fs'; +import vm from 'node:vm'; + +const source=readFileSync(new URL('../assets/dashboard.js',import.meta.url),'utf8'); +const start=source.indexOf("document.addEventListener('click'"); +const end=source.indexOf('const BRAINS_READY=',start); +assert.ok(start>=0 && end>start); +const handlers={}, menu={open:true,contains:()=>false}; +let focused=false; +vm.runInNewContext(source.slice(start,end),{ + document:{addEventListener:(event,handler)=>handlers[event]=handler}, + $:selector=>selector==='#brain-controls'?menu:{focus:()=>{focused=true;}}, +}); + +// Selecting a brain replaces the clicked button before the event reaches document. +handlers.click({target:{},composedPath:()=>[{},menu,{}]}); +assert.equal(menu.open,true,'Selecting a brain must leave the menu open'); +handlers.click({target:{},composedPath:()=>[{}]}); +assert.equal(menu.open,false,'Clicking outside must close the menu'); +menu.open=true; +handlers.keydown({key:'Tab'}); +assert.equal(menu.open,true); +handlers.keydown({key:'Escape'}); +assert.equal(menu.open,false); +assert.equal(focused,true,'Escape must return focus to the menu trigger');