From 59f4f54e518db3323cd7aaabfdc48ecb2bdaf40d Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 14 Jun 2026 19:09:37 -0500 Subject: feat(theme-studio): auto-dim split preview auto-dim-other-buffers is a package face, not a theme face, so build-inventory.el (it scans only elpa/straight packages) never listed it and the studio couldn't theme it. This adds it as a bespoke app. The preview is a vertical split: the focused window on the left in real syntax colors, the same code on the right collapsed to the single auto-dim-other-buffers face, the way Emacs renders a non-selected window. Both panes follow the language selector. A trailing row shows auto-dim-other-buffers-hide, whose foreground matches the background so it vanishes when dimmed. A #autodimtest gate covers the split, the uniform recolor, and language sync. --- scripts/theme-studio/app.js | 40 +++++++++++++ scripts/theme-studio/app_inventory.py | 1 + scripts/theme-studio/browser-gates.js | 19 +++++++ scripts/theme-studio/face_data.py | 6 ++ scripts/theme-studio/generate.py | 1 + scripts/theme-studio/run-tests.sh | 2 +- scripts/theme-studio/theme-studio.html | 76 ++++++++++++++++++++++++- scripts/theme-studio/theme-studio.template.html | 2 +- 8 files changed, 142 insertions(+), 5 deletions(-) (limited to 'scripts') diff --git a/scripts/theme-studio/app.js b/scripts/theme-studio/app.js index 5056a7be8..e6951384a 100644 --- a/scripts/theme-studio/app.js +++ b/scripts/theme-studio/app.js @@ -828,7 +828,47 @@ function renderTelegaPreview(){const a='telega',L=[]; L.push('Webpage '+os(a,'telega-webpage-title','Title')+' '+os(a,'telega-webpage-subtitle','Subtitle')+' '+os(a,'telega-webpage-header','Header')+' '+os(a,'telega-webpage-subheader','Subheader')+' '+os(a,'telega-webpage-outline','outline')+' '+os(a,'telega-webpage-fixed','fixed')+' '+os(a,'telega-webpage-preformatted','pre')+' '+os(a,'telega-webpage-marked','marked')+' '+os(a,'telega-webpage-strike-through','strike')+' '+os(a,'telega-webpage-chat-link','chat-link')); return `
${L.join('\n')}
`;} function genericPreview(app){let h='
';for(const [face,label] of APPS[app].faces)h+=`
${esc(label)}
`;return h+'
';} +// Bespoke split preview: a focused window beside its auto-dimmed twin, both +// showing the language selected at the top of the page (kept in sync via the +// langsel onchange, which re-runs buildPkgPreview). The left pane carries the +// real per-token syntax colors; the right pane shows what auto-dim does -- every +// default/font-lock face remaps to the single `auto-dim-other-buffers' face, so +// the same code collapses to one faded foreground on the dim background. The +// trailing row demonstrates `auto-dim-other-buffers-hide' (org hidden text whose +// foreground matches the background, so it vanishes in a dimmed window). +function renderAutodimPreview(){ + const a='auto-dim-other-buffers'; + const langsel=document.getElementById('langsel'); + const lang=(langsel&&langsel.value)||Object.keys(SAMPLES)[0]; + const lines=(SAMPLES[lang]||[]).slice(0,9); + let lit=''; + for(const line of lines){ + if(!line.length){lit+='\n';continue;} + for(const [k,t] of line)lit+=`${esc(t)}`; + lit+='\n';} + const dimFg=effFg(pkgEffFg(a,'auto-dim-other-buffers')),dimBg=pkgEffBg(a,'auto-dim-other-buffers')||'#000000'; + let dim=''; + for(const line of lines){ + if(!line.length){dim+='\n';continue;} + for(const [,t] of line)dim+=esc(t); + dim+='\n';} + const hideFg=effFg(pkgEffFg(a,'auto-dim-other-buffers-hide')),hideBg=pkgEffBg(a,'auto-dim-other-buffers-hide')||dimBg; + const foldText='··· folded body (hidden when dimmed) ···'; + const accent=uf('cursor').bg||'#67809c'; + const pane=(label,body,bg,focused)=> + `
` + +`
${label}
` + +`
${body}
`; + const litBody=lit+'\n'+`${esc(foldText)}`; + const dimBody=`${dim}\n` + +`${esc(foldText)}`; + return `
` + +pane('normal',litBody,MAP['bg'],true) + +pane('auto-dim',dimBody,dimBg,false) + +`
`; +} const PACKAGE_PREVIEWS={ + autodim:renderAutodimPreview, org:renderOrgPreview,magit:renderMagitPreview,elfeed:renderElfeedPreview,ghostel:renderGhostelPreview, dashboard:renderDashboardPreview,mu4e:renderMu4ePreview,lsp:renderLspPreview,gitgutter:renderGitGutterPreview, flycheck:renderFlycheckPreview,dired:renderDiredPreview,dirvish:renderDirvishPreview,calibredb:renderCalibredbPreview, diff --git a/scripts/theme-studio/app_inventory.py b/scripts/theme-studio/app_inventory.py index e491b8abb..28b641a94 100644 --- a/scripts/theme-studio/app_inventory.py +++ b/scripts/theme-studio/app_inventory.py @@ -14,6 +14,7 @@ BESPOKE_APPS = { "org-mode", "mu4e", "ghostel", + "auto-dim-other-buffers", "dashboard", "lsp-mode", "git-gutter", diff --git a/scripts/theme-studio/browser-gates.js b/scripts/theme-studio/browser-gates.js index 3e5e12628..41b459b63 100644 --- a/scripts/theme-studio/browser-gates.js +++ b/scripts/theme-studio/browser-gates.js @@ -223,6 +223,25 @@ if(location.hash==='#generatortest'){let ok=true;const notes=[];const A=(c,n)=>{ A(document.querySelector('#genpreview .genchip .gn').textContent==='medium aquamarine','generated tile names display spaces instead of hyphens'); document.title='GENERATORTEST '+(ok?'PASS':'FAIL'); const d=document.createElement('div');d.id='generatortest';d.textContent='GENERATORTEST '+(ok?'PASS':'FAIL')+(notes.length?' | '+notes.join(' ; '):'');document.body.appendChild(d);} +// Auto-dim gate (open with #autodimtest): the bespoke split preview shows the +// selected language in both panes -- the left in real syntax colors, the right +// collapsed to the single auto-dim-other-buffers face -- and tracks the langsel. +if(location.hash==='#autodimtest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}}; + const langs=Object.keys(SAMPLES),ls=document.getElementById('langsel'); + ls.value=langs[0]; + const box=document.createElement('div');box.innerHTML=renderAutodimPreview(); + A(box.innerHTML.includes('>normal<'),'left pane has a "normal" header'); + A(box.innerHTML.includes('>auto-dim<'),'right pane has an "auto-dim" header'); + A(box.querySelectorAll('[data-k]').length>0,'focused pane carries per-token syntax spans'); + const dimFace=box.querySelector('[data-face="auto-dim-other-buffers"]'); + A(!!dimFace,'dimmed pane uses the auto-dim-other-buffers face'); + A(dimFace&&dimFace.querySelectorAll('[data-k]').length===0,'dimmed code is uniform, no per-token syntax'); + A(!!box.querySelector('[data-face="auto-dim-other-buffers-hide"]'),'the hide face is represented'); + if(langs.length>1){const t1=box.textContent;ls.value=langs[1]; + const box2=document.createElement('div');box2.innerHTML=renderAutodimPreview(); + A(box2.textContent!==t1,'preview tracks the language selector');ls.value=langs[0];} + document.title='AUTODIMTEST '+(ok?'PASS':'FAIL'); + const d=document.createElement('div');d.id='autodimtest';d.textContent='AUTODIMTEST '+(ok?'PASS':'FAIL')+(notes.length?' | '+notes.join(' ; '):'');document.body.appendChild(d);} if(location.hash.startsWith('#pick')){openPicker();const m=location.hash.slice(5);if(m){const b=document.querySelector('.pmode button[data-m="'+m+'"]');if(b)b.click();}} if(location.hash==='#cursortest'){document.getElementById('newhexstr').value='#67809c';openPicker();const sc=document.getElementById('svcur'),hc=document.getElementById('huecur');const L=parseFloat(sc.style.left||'0'),T=parseFloat(sc.style.top||'0'),H=parseFloat(hc.style.top||'0');const ok=L>1&&T>1&&H>1;document.title='CURSORTEST '+(ok?'PASS':'FAIL');const d=document.createElement('div');d.id='cursortest';d.textContent='CURSORTEST '+(ok?'PASS':'FAIL')+' left='+sc.style.left+' top='+sc.style.top+' hue='+hc.style.top;document.body.appendChild(d);} if(location.hash.startsWith('#app')){const ap=location.hash.slice(4),s=document.getElementById('appsel');if(s&&ap){s.value=ap;pkgChanged();}} diff --git a/scripts/theme-studio/face_data.py b/scripts/theme-studio/face_data.py index d5999f816..a662d612c 100644 --- a/scripts/theme-studio/face_data.py +++ b/scripts/theme-studio/face_data.py @@ -143,6 +143,12 @@ GHOSTEL_SEED={ "ghostel-color-blue":{"fg":"blue"},"ghostel-color-magenta":{"fg":"regal"},"ghostel-color-cyan":{"fg":"sage"},"ghostel-color-white":{"fg":"silver"}, "ghostel-color-bright-black":{"fg":"steel"},"ghostel-color-bright-red":{"fg":"#de4949"},"ghostel-color-bright-green":{"fg":"#84b068"},"ghostel-color-bright-yellow":{"fg":"#eed376"}, "ghostel-color-bright-blue":{"fg":"#7a9abe"},"ghostel-color-bright-magenta":{"fg":"#b07fd0"},"ghostel-color-bright-cyan":{"fg":"#7fc0a8"},"ghostel-color-bright-white":{"fg":"white"}} +# auto-dim-other-buffers: non-selected windows recede to a faded fg on a near-black +# bg. The -hide face keeps org hidden text invisible in a dimmed window (fg=bg). +AUTODIM_FACES=("auto-dim-other-buffers auto-dim-other-buffers-hide").split() +AUTODIM_SEED={ + "auto-dim-other-buffers":{"fg":"#5e6770","bg":"#000000"}, + "auto-dim-other-buffers-hide":{"fg":"#000000","bg":"#000000"}} DASHBOARD_FACES=("dashboard-banner-logo-title dashboard-text-banner dashboard-heading " "dashboard-items-face dashboard-navigator dashboard-no-items-face dashboard-footer-face dashboard-footer-icon-face").split() DASHBOARD_SEED={ diff --git a/scripts/theme-studio/generate.py b/scripts/theme-studio/generate.py index aa0e829fb..e3ec3981c 100644 --- a/scripts/theme-studio/generate.py +++ b/scripts/theme-studio/generate.py @@ -227,6 +227,7 @@ APPS={"org-mode":{"label":"org-mode","preview":"org","faces":face_rows(ORG_FACES "elfeed":{"label":"elfeed","preview":"elfeed","faces":face_rows(ELFEED_FACES,"elfeed-",ELFEED_SEED)}, "mu4e":{"label":"mu4e","preview":"mu4e","faces":face_rows(MU4E_FACES,"mu4e-",MU4E_SEED)}, "ghostel":{"label":"ghostel","preview":"ghostel","faces":face_rows(GHOSTEL_FACES,"ghostel-",GHOSTEL_SEED)}, + "auto-dim-other-buffers":{"label":"auto-dim","preview":"autodim","faces":face_rows(AUTODIM_FACES,"auto-dim-other-buffers-",AUTODIM_SEED)}, "dashboard":{"label":"dashboard","preview":"dashboard","faces":face_rows(DASHBOARD_FACES,"dashboard-",DASHBOARD_SEED)}, "lsp-mode":{"label":"lsp-mode","preview":"lsp","faces":face_rows(LSP_FACES,"lsp-",LSP_SEED)}, "git-gutter":{"label":"git-gutter","preview":"gitgutter","faces":face_rows(GITGUTTER_FACES,"git-gutter:",GITGUTTER_SEED)}, diff --git a/scripts/theme-studio/run-tests.sh b/scripts/theme-studio/run-tests.sh index f6a68c41b..2a6d332ac 100755 --- a/scripts/theme-studio/run-tests.sh +++ b/scripts/theme-studio/run-tests.sh @@ -55,7 +55,7 @@ CHROME="" for c in google-chrome-stable google-chrome chromium chromium-browser; do if command -v "$c" >/dev/null 2>&1; then CHROME="$c"; break; fi done -HASHES="selftest cursortest readouttest deltatest oklchtest planetest locktest sorttest mocktest contrasttest safetest healtest columntest counttest baseedittest roundtriptest beveltest previewlinktest generatortest" +HASHES="selftest cursortest readouttest deltatest oklchtest planetest locktest sorttest mocktest contrasttest safetest healtest columntest counttest baseedittest roundtriptest beveltest previewlinktest generatortest autodimtest" if [ "$NO_BROWSER" = 1 ]; then skip_msg "browser hash gates (--no-browser)" elif [ -z "$CHROME" ]; then diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html index d05ac912f..16b112f39 100644 --- a/scripts/theme-studio/theme-studio.html +++ b/scripts/theme-studio/theme-studio.html @@ -207,7 +207,7 @@
elements △fg △bg △styleboxcontrastexample
-
+

  
@@ -240,7 +240,7 @@