From 85c1fcf7925f5d74bbbe8e36c56ee18f7f1314ec Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Tue, 9 Jun 2026 12:33:51 -0500 Subject: feat(theme-studio): add a real, exported :box face attribute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mode-line box in the preview was hardcoded — it showed a box the generated theme couldn't actually produce, since build-theme.el never emitted :box. Made :box a real face attribute instead: a per-face box object (style line/raised/pressed, width, color) stored on UI and package faces, set from a "box" dropdown in both tables, rendered from the attribute everywhere (the mode-line bars, the package previews via ofs, the UI table preview cells), and exported through build-theme.el's --attrs as a proper :box plist (released/pressed → :style *-button; line → :line-width + optional :color). The hardcoded box is gone; mode-line and mode-line-inactive now default to the released-button box that is the Emacs default, so the preview and the export agree. This also gives the package faces that genuinely use :box a way to represent it — the face audit found several (magit-branch-current/-remote-head, two flycheck list faces, the telega button family, ~15 slack button/dialog faces). Tests: build-theme gains box-conversion + ui-box-emit ERT tests (24/24); the app-core deep-equal tests account for the new box slot; all 9 browser gates, 20 python, and 55 node tests stay green. --- scripts/theme-studio/app.js | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) (limited to 'scripts/theme-studio/app.js') diff --git a/scripts/theme-studio/app.js b/scripts/theme-studio/app.js index 450183e0..d0fed81c 100644 --- a/scripts/theme-studio/app.js +++ b/scripts/theme-studio/app.js @@ -300,6 +300,20 @@ async function importTheme(){ function applyGround(){document.querySelectorAll('pre').forEach(p=>p.style.background=MAP['bg']);document.querySelectorAll('.ex').forEach(e=>e.style.background=MAP['bg']);} function uf(f){return UIMAP[f]||{};} function udeco(o){return `font-weight:${o.bold?'bold':'normal'};font-style:${o.italic?'italic':'normal'};text-decoration:${(o.underline?'underline ':'')+(o.strike?'line-through':'')||'none'}`;} +// A face's :box, rendered as an inset box-shadow (no layout shift). Returns the +// box-shadow VALUE (or '' for no box). 'line' is a flat border in the box color +// (or the face's own color when unset); 'released'/'pressed' are the 3D button +// styles Emacs draws, derived from the background so they read on any color. +function boxCss(b){if(!b||!b.style)return '';const w=b.width||1; + if(b.style==='released')return `inset ${w}px ${w}px 0 #ffffff33,inset -${w}px -${w}px 0 #00000066`; + if(b.style==='pressed')return `inset ${w}px ${w}px 0 #00000066,inset -${w}px -${w}px 0 #ffffff33`; + return `inset 0 0 0 ${w}px ${b.color||'currentColor'}`;} +// The per-row box control: none / line / raised / pressed. get()/set() read and +// write the face's box object (null = no box). +function mkBoxSelect(get,set){const s=document.createElement('select');s.className='chip';s.style.cssText='width:84px;font:10pt monospace'; + [['','no box'],['line','line'],['released','raised'],['pressed','pressed']].forEach(([v,l])=>{const o=document.createElement('option');o.value=v;o.textContent=l;s.appendChild(o);}); + const cur=get();s.value=cur&&cur.style?cur.style:''; + s.onchange=()=>set(s.value?{style:s.value,width:1,color:null}:null);return s;} function flashRow(tr){if(!tr)return;tr.scrollIntoView({block:'center',behavior:'smooth'});tr.classList.remove('flash');void tr.offsetWidth;tr.classList.add('flash');} function flashEl(el){if(!el)return;el.scrollIntoView({block:'nearest',inline:'nearest',behavior:'smooth'});el.classList.remove('flashtok');void el.offsetWidth;el.classList.add('flashtok');} // Flash every matching element but scroll only the first into view, so a face @@ -376,9 +390,9 @@ function buildMockFrame(){ buf+=`
${L.cont?'↪':''}${i+1}${cd||' '}
`; }); let html=`
${buf}
`; - const mlbox='box-shadow:inset 1px 1px 0 #ffffff33,inset -1px -1px 0 #00000066'; // 3D released-button box, the Emacs mode-line default - html+=`
init.el (Emacs Lisp) L5 git:main
`; - html+=`
*Messages* (Fundamental)
`; + const mlbx=boxCss(ml.box),mlibx=boxCss(mli.box); + html+=`
init.el (Emacs Lisp) L5 git:main
`; + html+=`
*Messages* (Fundamental)
`; html+=`
I-search: count zzz [no match]
`; html+=`
https://gnu.org error warning ok
`; fr.innerHTML=html;fr.style.background=bg;fr.style.color=fg; @@ -390,7 +404,7 @@ function buildMockFrame(){ function uiSelect(face,attr){const cur=UIMAP[face][attr]||''; return mkColorDropdown(ddList(cur),cur,h=>{UIMAP[face][attr]=h||null;paintUI(face);buildMockFrame();});} const BASE_INHERITS=['fixed-pitch','variable-pitch','default','link','bold','italic','shadow']; -function seedFace(d){return {fg:pname(d.fg),bg:pname(d.bg),bold:!!d.bold,italic:!!d.italic,underline:!!d.underline,strike:!!d.strike,inherit:d.inherit||null,height:d.height||1,source:'default'};} +function seedFace(d){return {fg:pname(d.fg),bg:pname(d.bg),bold:!!d.bold,italic:!!d.italic,underline:!!d.underline,strike:!!d.strike,inherit:d.inherit||null,height:d.height||1,box:d.box||null,source:'default'};} function curApp(){const s=document.getElementById('appsel');return s&&s.value?s.value:Object.keys(APPS)[0];} function pkgEffFg(app,face,seen){return effResolve(PKGMAP,app,face,'fg',seen);} function pkgEffBg(app,face,seen){return effResolve(PKGMAP,app,face,'bg',seen);} @@ -414,13 +428,14 @@ function buildPkgTable(){ const ci=document.createElement('td');const isel=document.createElement('select');isel.className='chip';isel.style.cssText='width:150px;font:10pt monospace';inh.forEach(o=>{const op=document.createElement('option');op.value=o;op.textContent=o||'— none —';isel.appendChild(op);});isel.value=f.inherit||'';isel.onchange=()=>{f.inherit=isel.value||null;f.source='user';pkgChanged();};ci.appendChild(isel); const ch=document.createElement('td');const hin=document.createElement('input');hin.type='number';hin.min='0.8';hin.max='2.5';hin.step='0.05';hin.value=f.height||1;hin.className='hstep';hin.onchange=()=>{f.height=parseFloat(hin.value)||1;f.source='user';pkgChanged();};ch.appendChild(hin); const cc=document.createElement('td');cc.style.fontSize='10pt';cc.style.whiteSpace='nowrap';const efg=effFg(pkgEffFg(app,face)),ebg=effBg(pkgEffBg(app,face)),r=contrast(efg,ebg);cc.innerHTML=crHtml(r); + const cx=document.createElement('td');const boxSel=mkBoxSelect(()=>f.box,b=>{f.box=b;f.source='user';pkgChanged();});cx.appendChild(boxSel); const cr=document.createElement('td');const rb=document.createElement('button');rb.className='sbtn';rb.textContent='↺';rb.title='reset to default';rb.onclick=()=>{PKGMAP[app][face]=seedFace(def);pkgChanged();};cr.appendChild(rb); - const cL=mkLockCell('pkg:'+app+':'+face,[fgd,bgd,...pkBtns,isel,hin,rb]); - tr.append(c0,cL,cf,cb,cw,cc,ci,ch,cr);tb.appendChild(tr); + const cL=mkLockCell('pkg:'+app+':'+face,[fgd,bgd,...pkBtns,isel,hin,boxSel,rb]); + tr.append(c0,cL,cf,cb,cw,cc,ci,ch,cx,cr);tb.appendChild(tr); } applyTableSort('pkgbody'); } -function ofs(app,face){const f=PKGMAP[app][face]||{},fg=effFg(pkgEffFg(app,face)),bg=pkgEffBg(app,face);const dec=(f.underline?'underline ':'')+(f.strike?'line-through':'');return `color:${fg};${bg?'background:'+bg+';':''}font-weight:${f.bold?'bold':'normal'};font-style:${f.italic?'italic':'normal'};text-decoration:${dec.trim()||'none'};font-size:${(f.height||1)}em`;} +function ofs(app,face){const f=PKGMAP[app][face]||{},fg=effFg(pkgEffFg(app,face)),bg=pkgEffBg(app,face);const dec=(f.underline?'underline ':'')+(f.strike?'line-through':'');const bx=boxCss(f.box);return `color:${fg};${bg?'background:'+bg+';':''}font-weight:${f.bold?'bold':'normal'};font-style:${f.italic?'italic':'normal'};text-decoration:${dec.trim()||'none'};font-size:${(f.height||1)}em${bx?';box-shadow:'+bx:''}`;} function os(app,face,txt){return `${txt}`;} function renderOrgPreview(){const a='org-mode',L=[]; L.push(os(a,'org-document-info-keyword','#+TITLE:')+' '+os(a,'org-document-title','Project Notes')); @@ -721,7 +736,7 @@ function genericPreview(app){let h='