diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-24 16:47:16 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-24 16:47:16 -0400 |
| commit | b1fba1d65a02a18a8d063b31201a16f9714c9378 (patch) | |
| tree | 6dca4cc7f704de9969effe517f84401eb9203357 /scripts/theme-studio | |
| parent | c3f6e2a4fbfd08ca5482f23c8b23f07567d43162 (diff) | |
| download | dotemacs-b1fba1d65a02a18a8d063b31201a16f9714c9378.tar.gz dotemacs-b1fba1d65a02a18a8d063b31201a16f9714c9378.zip | |
feat(theme-studio): visible size-nav buttons + 48 pt gallery scale
The preview dropdown gets flanking nav buttons, matching the view selector, so the size steps with a click. Left/Right arrows do the same when the dropdown is focused. Both clamp at the ends and disable on a single-pane app.
I extended the size scale to 32 and 48 pt for inspecting a glyph's detail. The cell width scales with the size, so beyond about 48 pt the grid is mostly scrolling.
I removed the separate hover info line beside the dropdown. Each glyph's own title tooltip already shows its face and color, so the line was redundant.
A new computed-style gate confirms the point size renders to the right pixels (24 pt is 32 px), so the pt label isn't lying.
Diffstat (limited to 'scripts/theme-studio')
| -rw-r--r-- | scripts/theme-studio/app.js | 31 | ||||
| -rw-r--r-- | scripts/theme-studio/browser-gates.js | 34 | ||||
| -rw-r--r-- | scripts/theme-studio/theme-studio.html | 67 | ||||
| -rw-r--r-- | scripts/theme-studio/theme-studio.template.html | 2 |
4 files changed, 82 insertions, 52 deletions
diff --git a/scripts/theme-studio/app.js b/scripts/theme-studio/app.js index 338d84743..b50315981 100644 --- a/scripts/theme-studio/app.js +++ b/scripts/theme-studio/app.js @@ -772,7 +772,7 @@ const PACKAGE_PREVIEWS={ // name and is disabled). nerd-icons is the one multi-pane app: one pane per font // size, so the designer can view the icon grid at different sizes — pt because // Emacs sizes fonts in :height (1/10 pt), so a pane maps to a real buffer size. -const NERD_ICON_SIZES_PT=[10,12,14,16,20,24]; +const NERD_ICON_SIZES_PT=[10,12,14,16,20,24,32,48]; const NERD_ICON_DEFAULT_PT=14; function previewPanes(app){ // Multi-pane only when nerd-icons actually has a gallery to size. If the gallery @@ -789,6 +789,13 @@ function defaultPaneIdx(app){ } // Per-app selected pane index, so a chosen size survives edits and revisits. const PREV_PANE={}; +// The ‹ › buttons flanking the preview dropdown (and the Left/Right arrows) step the +// pane by DIR, clamped, and re-render. No-op on a disabled (single-pane) dropdown. +function stepPreviewPane(dir){ + const s=document.getElementById('pkgprevsel');if(!s||s.disabled)return; + const i=stepViewIndex(s.selectedIndex,s.options.length,dir); + if(i!==s.selectedIndex){PREV_PANE[curApp()]=i;buildPkgPreview();} +} function buildPkgPreview(){ const app=curApp(),p=document.getElementById('pkgpreview');if(!p)return; rebuildLocateRegistry(); @@ -808,25 +815,21 @@ function buildPkgPreview(){ sel.value=String(idx); sel.disabled=panes.length<2; sel.onchange=()=>{PREV_PANE[app]=+sel.value;buildPkgPreview();}; - // Left/Right arrows step through the panes when the dropdown is focused - // (Up/Down already do, natively); clamped at the ends. Re-render and refocus, - // since rebuilding the options would otherwise drop keyboard focus. + // Left/Right arrows step the panes when the dropdown is focused (Up/Down already + // do, natively); re-grab + refocus the select, since the rebuild drops focus. sel.onkeydown=(e)=>{ if(e.key!=='ArrowLeft'&&e.key!=='ArrowRight')return; e.preventDefault(); - const cur=+sel.value,nxt=e.key==='ArrowRight'?Math.min(cur+1,panes.length-1):Math.max(cur-1,0); - if(nxt===cur)return; - PREV_PANE[app]=nxt;buildPkgPreview(); + stepPreviewPane(e.key==='ArrowRight'?1:-1); const s=document.getElementById('pkgprevsel');if(s)s.focus(); }; + // The flanking ‹ › buttons follow the dropdown's enabled state. + const pb=document.getElementById('pkgprevprev'),nb=document.getElementById('pkgprevnext'); + if(pb)pb.disabled=panes.length<2; + if(nb)nb.disabled=panes.length<2; } - // Immediate-wayfinding info line: hovering an element shows "section > face — - // value" next to the dropdown (the element's title is the deterministic - // fallback); leaving the preview clears it. - const info=document.getElementById('pkgprevinfo'); - if(info)info.textContent=''; - p.onmouseover=(e)=>{const u=e.target.closest('[data-owner-app]');if(!u||!info)return;info.textContent=locateInfoLine(locateFaceMeta(u.dataset.ownerApp,u.dataset.face,LOCATE_REG));}; - p.onmouseleave=()=>{if(info)info.textContent='';}; + // Per-element wayfinding rides each preview span's own hover title (face + value); + // no separate info line. } function resetApp(){const app=curApp();for(const [face,,d] of APPS[app].faces)if(!LOCKED.has('pkg:'+app+':'+face))PKGMAP[app][face]=seedFace(d);pkgChanged();notify('reset editable '+app+' faces to package defaults',false);} function syncPkgHeight(){const t=document.getElementById('pkgtable'),m=document.getElementById('pkgpreview');if(!t||!m)return;const lb=m.previousElementSibling,lbh=lb?lb.getBoundingClientRect().height+10:30;m.style.height=Math.max(t.getBoundingClientRect().height-lbh,220)+'px';} diff --git a/scripts/theme-studio/browser-gates.js b/scripts/theme-studio/browser-gates.js index 1157b0712..0bc6b2fbd 100644 --- a/scripts/theme-studio/browser-gates.js +++ b/scripts/theme-studio/browser-gates.js @@ -891,8 +891,30 @@ if(location.hash==='#previewpanetest')gate('previewpanetest',A=>{ const sel=document.getElementById('pkgprevsel'); A(+sel.value===defaultPaneIdx('nerd-icons'),'a stale pane index resets to the default'); A(!sel.disabled&&sel.options.length===NERD_ICON_SIZES_PT.length,'nerd-icons: dropdown enabled with one option per size'); + // Left/Right arrows step the size, clamped at the ends. + PREV_PANE['nerd-icons']=0;buildPkgPreview(); + document.getElementById('pkgprevsel').dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowRight',bubbles:true})); + A(PREV_PANE['nerd-icons']===1,'ArrowRight steps to the next size'); + document.getElementById('pkgprevsel').dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowLeft',bubbles:true})); + document.getElementById('pkgprevsel').dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowLeft',bubbles:true})); + A(PREV_PANE['nerd-icons']===0,'ArrowLeft steps back and clamps at the first size'); + // The visible ‹ › buttons step the size too, and clamp. + PREV_PANE['nerd-icons']=0;buildPkgPreview(); + document.getElementById('pkgprevnext').click(); + A(PREV_PANE['nerd-icons']===1,'the > button steps to the next size'); + document.getElementById('pkgprevprev').click(); + document.getElementById('pkgprevprev').click(); + A(PREV_PANE['nerd-icons']===0,'the < button steps back and clamps at the first size'); + A(!document.getElementById('pkgprevprev').disabled&&!document.getElementById('pkgprevnext').disabled,'the nav buttons are enabled when multi-pane'); + // The glyph actually computes to the selected point size (pt -> px): 24 pt = 32 px. + PREV_PANE['nerd-icons']=NERD_ICON_SIZES_PT.indexOf(24);buildPkgPreview(); + const gw=document.querySelector('#pkgpreview .ni-cell > span'); + const gpx=gw?parseFloat(getComputedStyle(gw).fontSize):0; + A(Math.abs(gpx-32)<2,'24 pt glyph computes to ~32 px, so the point size renders to size ('+gpx+' px)'); + PREV_PANE['nerd-icons']=defaultPaneIdx('nerd-icons'); vs.value=single;buildPkgPreview(); A(sel.disabled&&sel.options.length===1,'single-pane app: dropdown disabled with one option'); + A(document.getElementById('pkgprevprev').disabled&&document.getElementById('pkgprevnext').disabled,'single-pane app: the nav buttons are disabled too'); } vs.value=saved;buildPkgPreview(); } @@ -1295,17 +1317,7 @@ if(location.hash==='#locatehovertest')gate('locatehovertest',A=>withSavedState([ rebuildLocateRegistry(); const cb=document.createElement('div');cb.innerHTML=os(app,face,'x'); A(/cleared, rendering as default/.test(cb.querySelector('[data-face]').getAttribute('title')),'cleared face title carries the cleared-rendering note'); - // info line on hover — now a dedicated span next to the pane dropdown, cleared on leave - PKGMAP[app][face]={fg:'#abcdef',bg:null,inherit:null,source:'user'}; - buildPkgPreview(); - const p=document.getElementById('pkgpreview'),info=document.getElementById('pkgprevinfo'); - rebuildLocateRegistry(); - p.innerHTML=os(app,face,'hover me'); - p.querySelector('[data-owner-app]').dispatchEvent(new MouseEvent('mouseover',{bubbles:true})); - A(info.textContent===locateInfoLine(locateFaceMeta(app,face,LOCATE_REG)),'hover updates the info line to section > face — value: '+info.textContent); - A(/ > .* — /.test(info.textContent),'info line uses the section > face — value shape'); - p.dispatchEvent(new MouseEvent('mouseleave')); - A(info.textContent==='','leaving the preview clears the info line'); + // Wayfinding is the per-span hover title (above); there is no separate info line. })); // Click + cursor gate (open with #locateclicktest): an on-pane element carries the // locate-onpane class (pointer cursor) and clicking flashes its assignment row via diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html index efcf6290d..234f3b7c7 100644 --- a/scripts/theme-studio/theme-studio.html +++ b/scripts/theme-studio/theme-studio.html @@ -285,7 +285,7 @@ <table class="leg" id="pkgtable"><thead><tr><th title="lock a decided face"></th><th onclick="srtTable('pkgbody',1)">face △</th><th onclick="srtTable('pkgbody',2)">fg △</th><th onclick="srtTable('pkgbody',3)">bg △</th><th>style</th><th title="face :box (border)">box</th><th onclick="srtTable('pkgbody',6)">contrast △</th></tr></thead><tbody id="pkgbody"></tbody></table> </section> <section class="pane grow" style="display:flex;flex-direction:column"> - <div class="langbar"><label style="color:#b4b1a2">preview: </label><select id="pkgprevsel" style="background:#3a3d44;color:#e8e8ea;border:1px solid #5a5d68;border-radius:3px;padding:1px 4px;font:inherit"></select> <span id="pkgprevinfo" style="color:#b4b1a2"></span></div> + <div class="langbar"><label style="color:#b4b1a2">preview: </label><button id="pkgprevprev" class="viewnav" title="previous size" onclick="stepPreviewPane(-1)">‹</button><select id="pkgprevsel" style="background:#3a3d44;color:#e8e8ea;border:1px solid #5a5d68;border-radius:3px;padding:1px 4px;font:inherit"></select><button id="pkgprevnext" class="viewnav" title="next size" onclick="stepPreviewPane(1)">›</button></div> <div id="pkgpreview" class="mock" style="overflow:auto;min-height:60vh"></div> </section> </div> @@ -3215,7 +3215,7 @@ const PACKAGE_PREVIEWS={ // name and is disabled). nerd-icons is the one multi-pane app: one pane per font // size, so the designer can view the icon grid at different sizes — pt because // Emacs sizes fonts in :height (1/10 pt), so a pane maps to a real buffer size. -const NERD_ICON_SIZES_PT=[10,12,14,16,20,24]; +const NERD_ICON_SIZES_PT=[10,12,14,16,20,24,32,48]; const NERD_ICON_DEFAULT_PT=14; function previewPanes(app){ // Multi-pane only when nerd-icons actually has a gallery to size. If the gallery @@ -3232,6 +3232,13 @@ function defaultPaneIdx(app){ } // Per-app selected pane index, so a chosen size survives edits and revisits. const PREV_PANE={}; +// The ‹ › buttons flanking the preview dropdown (and the Left/Right arrows) step the +// pane by DIR, clamped, and re-render. No-op on a disabled (single-pane) dropdown. +function stepPreviewPane(dir){ + const s=document.getElementById('pkgprevsel');if(!s||s.disabled)return; + const i=stepViewIndex(s.selectedIndex,s.options.length,dir); + if(i!==s.selectedIndex){PREV_PANE[curApp()]=i;buildPkgPreview();} +} function buildPkgPreview(){ const app=curApp(),p=document.getElementById('pkgpreview');if(!p)return; rebuildLocateRegistry(); @@ -3251,25 +3258,21 @@ function buildPkgPreview(){ sel.value=String(idx); sel.disabled=panes.length<2; sel.onchange=()=>{PREV_PANE[app]=+sel.value;buildPkgPreview();}; - // Left/Right arrows step through the panes when the dropdown is focused - // (Up/Down already do, natively); clamped at the ends. Re-render and refocus, - // since rebuilding the options would otherwise drop keyboard focus. + // Left/Right arrows step the panes when the dropdown is focused (Up/Down already + // do, natively); re-grab + refocus the select, since the rebuild drops focus. sel.onkeydown=(e)=>{ if(e.key!=='ArrowLeft'&&e.key!=='ArrowRight')return; e.preventDefault(); - const cur=+sel.value,nxt=e.key==='ArrowRight'?Math.min(cur+1,panes.length-1):Math.max(cur-1,0); - if(nxt===cur)return; - PREV_PANE[app]=nxt;buildPkgPreview(); + stepPreviewPane(e.key==='ArrowRight'?1:-1); const s=document.getElementById('pkgprevsel');if(s)s.focus(); }; + // The flanking ‹ › buttons follow the dropdown's enabled state. + const pb=document.getElementById('pkgprevprev'),nb=document.getElementById('pkgprevnext'); + if(pb)pb.disabled=panes.length<2; + if(nb)nb.disabled=panes.length<2; } - // Immediate-wayfinding info line: hovering an element shows "section > face — - // value" next to the dropdown (the element's title is the deterministic - // fallback); leaving the preview clears it. - const info=document.getElementById('pkgprevinfo'); - if(info)info.textContent=''; - p.onmouseover=(e)=>{const u=e.target.closest('[data-owner-app]');if(!u||!info)return;info.textContent=locateInfoLine(locateFaceMeta(u.dataset.ownerApp,u.dataset.face,LOCATE_REG));}; - p.onmouseleave=()=>{if(info)info.textContent='';}; + // Per-element wayfinding rides each preview span's own hover title (face + value); + // no separate info line. } function resetApp(){const app=curApp();for(const [face,,d] of APPS[app].faces)if(!LOCKED.has('pkg:'+app+':'+face))PKGMAP[app][face]=seedFace(d);pkgChanged();notify('reset editable '+app+' faces to package defaults',false);} function syncPkgHeight(){const t=document.getElementById('pkgtable'),m=document.getElementById('pkgpreview');if(!t||!m)return;const lb=m.previousElementSibling,lbh=lb?lb.getBoundingClientRect().height+10:30;m.style.height=Math.max(t.getBoundingClientRect().height-lbh,220)+'px';} @@ -4256,8 +4259,30 @@ if(location.hash==='#previewpanetest')gate('previewpanetest',A=>{ const sel=document.getElementById('pkgprevsel'); A(+sel.value===defaultPaneIdx('nerd-icons'),'a stale pane index resets to the default'); A(!sel.disabled&&sel.options.length===NERD_ICON_SIZES_PT.length,'nerd-icons: dropdown enabled with one option per size'); + // Left/Right arrows step the size, clamped at the ends. + PREV_PANE['nerd-icons']=0;buildPkgPreview(); + document.getElementById('pkgprevsel').dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowRight',bubbles:true})); + A(PREV_PANE['nerd-icons']===1,'ArrowRight steps to the next size'); + document.getElementById('pkgprevsel').dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowLeft',bubbles:true})); + document.getElementById('pkgprevsel').dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowLeft',bubbles:true})); + A(PREV_PANE['nerd-icons']===0,'ArrowLeft steps back and clamps at the first size'); + // The visible ‹ › buttons step the size too, and clamp. + PREV_PANE['nerd-icons']=0;buildPkgPreview(); + document.getElementById('pkgprevnext').click(); + A(PREV_PANE['nerd-icons']===1,'the > button steps to the next size'); + document.getElementById('pkgprevprev').click(); + document.getElementById('pkgprevprev').click(); + A(PREV_PANE['nerd-icons']===0,'the < button steps back and clamps at the first size'); + A(!document.getElementById('pkgprevprev').disabled&&!document.getElementById('pkgprevnext').disabled,'the nav buttons are enabled when multi-pane'); + // The glyph actually computes to the selected point size (pt -> px): 24 pt = 32 px. + PREV_PANE['nerd-icons']=NERD_ICON_SIZES_PT.indexOf(24);buildPkgPreview(); + const gw=document.querySelector('#pkgpreview .ni-cell > span'); + const gpx=gw?parseFloat(getComputedStyle(gw).fontSize):0; + A(Math.abs(gpx-32)<2,'24 pt glyph computes to ~32 px, so the point size renders to size ('+gpx+' px)'); + PREV_PANE['nerd-icons']=defaultPaneIdx('nerd-icons'); vs.value=single;buildPkgPreview(); A(sel.disabled&&sel.options.length===1,'single-pane app: dropdown disabled with one option'); + A(document.getElementById('pkgprevprev').disabled&&document.getElementById('pkgprevnext').disabled,'single-pane app: the nav buttons are disabled too'); } vs.value=saved;buildPkgPreview(); } @@ -4660,17 +4685,7 @@ if(location.hash==='#locatehovertest')gate('locatehovertest',A=>withSavedState([ rebuildLocateRegistry(); const cb=document.createElement('div');cb.innerHTML=os(app,face,'x'); A(/cleared, rendering as default/.test(cb.querySelector('[data-face]').getAttribute('title')),'cleared face title carries the cleared-rendering note'); - // info line on hover — now a dedicated span next to the pane dropdown, cleared on leave - PKGMAP[app][face]={fg:'#abcdef',bg:null,inherit:null,source:'user'}; - buildPkgPreview(); - const p=document.getElementById('pkgpreview'),info=document.getElementById('pkgprevinfo'); - rebuildLocateRegistry(); - p.innerHTML=os(app,face,'hover me'); - p.querySelector('[data-owner-app]').dispatchEvent(new MouseEvent('mouseover',{bubbles:true})); - A(info.textContent===locateInfoLine(locateFaceMeta(app,face,LOCATE_REG)),'hover updates the info line to section > face — value: '+info.textContent); - A(/ > .* — /.test(info.textContent),'info line uses the section > face — value shape'); - p.dispatchEvent(new MouseEvent('mouseleave')); - A(info.textContent==='','leaving the preview clears the info line'); + // Wayfinding is the per-span hover title (above); there is no separate info line. })); // Click + cursor gate (open with #locateclicktest): an on-pane element carries the // locate-onpane class (pointer cursor) and clicking flashes its assignment row via diff --git a/scripts/theme-studio/theme-studio.template.html b/scripts/theme-studio/theme-studio.template.html index 244e749e5..254a3a495 100644 --- a/scripts/theme-studio/theme-studio.template.html +++ b/scripts/theme-studio/theme-studio.template.html @@ -95,7 +95,7 @@ STYLES_CSS</style> <table class="leg" id="pkgtable"><thead><tr><th title="lock a decided face"></th><th onclick="srtTable('pkgbody',1)">face △</th><th onclick="srtTable('pkgbody',2)">fg △</th><th onclick="srtTable('pkgbody',3)">bg △</th><th>style</th><th title="face :box (border)">box</th><th onclick="srtTable('pkgbody',6)">contrast △</th></tr></thead><tbody id="pkgbody"></tbody></table> </section> <section class="pane grow" style="display:flex;flex-direction:column"> - <div class="langbar"><label style="color:#b4b1a2">preview: </label><select id="pkgprevsel" style="background:#3a3d44;color:#e8e8ea;border:1px solid #5a5d68;border-radius:3px;padding:1px 4px;font:inherit"></select> <span id="pkgprevinfo" style="color:#b4b1a2"></span></div> + <div class="langbar"><label style="color:#b4b1a2">preview: </label><button id="pkgprevprev" class="viewnav" title="previous size" onclick="stepPreviewPane(-1)">‹</button><select id="pkgprevsel" style="background:#3a3d44;color:#e8e8ea;border:1px solid #5a5d68;border-radius:3px;padding:1px 4px;font:inherit"></select><button id="pkgprevnext" class="viewnav" title="next size" onclick="stepPreviewPane(1)">›</button></div> <div id="pkgpreview" class="mock" style="overflow:auto;min-height:60vh"></div> </section> </div> |
