From b0d7b860f7b83866bfe7c26947449f13334d4a89 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 15 Jun 2026 18:04:54 -0500 Subject: feat(theme-studio): compact the box control into a 2x2 button cluster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The box control was a wide select plus a color swatch, pinning the box column at 166px. I replaced the select with a 2x2 cluster of radio buttons for the four styles: blank (no box), □ (line), ▼ (pressed), ▲ (raised). The color swatch now shows only while a box style is active, so the no-box case stays narrow. The column drops to 76px across all three tiers. A #boxtest gate covers the cluster: four buttons, radio selection, and the swatch hiding when no box is set. #beveltest now drives the style through the cluster button instead of the removed select. The same cluster shape sets up the B/I/U/S style column next. --- scripts/theme-studio/app.js | 26 +++++++++++----- scripts/theme-studio/browser-gates.js | 25 +++++++++++++-- scripts/theme-studio/styles.css | 6 +++- scripts/theme-studio/theme-studio.html | 57 +++++++++++++++++++++++++++------- 4 files changed, 92 insertions(+), 22 deletions(-) (limited to 'scripts') diff --git a/scripts/theme-studio/app.js b/scripts/theme-studio/app.js index 776f2742c..29d026ba0 100644 --- a/scripts/theme-studio/app.js +++ b/scripts/theme-studio/app.js @@ -381,15 +381,25 @@ function syntaxStyle(k){const s=syntaxFace(k),fg=(k==='bg'?MAP['p']:resolveSynta return `color:${fg};${bg?'background:'+bg+';':''}font-weight:${s.bold?'bold':'normal'};font-style:${s.italic?'italic':'normal'};text-decoration:${dec.trim()||'none'}${bx?';box-shadow:'+bx:''}`;} // The per-row box control: none / line / raised / pressed plus optional line // color. get()/set() read and write the face's box object (null = no box). +// Box control: a 2x2 cluster of radio buttons for the four box styles (no box / +// line / pressed / raised), plus a compact color swatch shown only while a box +// style is active. Replaces the old wide select+swatch to reclaim column width. function mkBoxControl(get,set,opts={}){const wrap=document.createElement('div');wrap.className='boxctl'; - 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 dd=mkColorDropdown(ddList((get()&&get().color)||''),(get()&&get().color)||'',h=>{const cur=get();if(!cur)return;set(Object.assign({},cur,{color:h||null}));},{compact:!!opts.compact,defaultHex:opts.defaultHex}); - function paint(){const cur=get();s.value=cur&&cur.style?cur.style:'';dd.setValue(cur&&cur.color?cur.color:''); - const off=!cur||!cur.style||wrap.dataset.locked==='1';dd.dataset.locked=off?'1':'';dd.classList.toggle('locked',off);if(dd.syncLocked)dd.syncLocked();} - s.onchange=()=>{const cur=get();set(s.value?{style:s.value,width:cur&&cur.width||1,color:cur&&cur.color||null}:null);paint();}; - wrap.syncLocked=()=>{const locked=wrap.dataset.locked==='1';s.disabled=locked;paint();}; - wrap.append(s,dd);paint();return wrap;} + const cluster=document.createElement('div');cluster.className='boxcluster'; + const states=[['','no box',''],['line','line box','□'],['pressed','pressed','▼'],['released','raised','▲']]; + const btns={}; + states.forEach(([v,title,glyph])=>{const b=document.createElement('button');b.className='boxbtn';b.dataset.style=v;b.textContent=glyph;b.title=title; + b.onclick=()=>{const cur=get();set(v?{style:v,width:(cur&&cur.width)||1,color:(cur&&cur.color)||null}:null);paint();}; + cluster.appendChild(b);btns[v]=b;}); + const dd=mkColorDropdown(ddList((get()&&get().color)||''),(get()&&get().color)||'',h=>{const cur=get();if(!cur)return;set(Object.assign({},cur,{color:h||null}));paint();},{compact:true,defaultHex:opts.defaultHex}); + function paint(){const cur=get(),style=cur&&cur.style?cur.style:''; + for(const v in btns)btns[v].classList.toggle('on',v===style); + dd.style.display=style?'':'none';dd.setValue(cur&&cur.color?cur.color:''); + const locked=wrap.dataset.locked==='1'; + for(const v in btns)btns[v].disabled=locked; + const ddoff=locked||!style;dd.dataset.locked=ddoff?'1':'';dd.classList.toggle('locked',ddoff);if(dd.syncLocked)dd.syncLocked();} + wrap.syncLocked=()=>paint(); + wrap.append(cluster,dd);paint();return wrap;} 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 diff --git a/scripts/theme-studio/browser-gates.js b/scripts/theme-studio/browser-gates.js index c47251a59..6fa353d9b 100644 --- a/scripts/theme-studio/browser-gates.js +++ b/scripts/theme-studio/browser-gates.js @@ -393,8 +393,8 @@ if(location.hash==='#beveltest'){let ok=true;const notes=[];const A=(c,n)=>{if(! A(bs3&&bs3.includes('rgb(255, 42, 42)')&&bs3.includes('rgb(143, 0, 0)'),'released style derives relief from explicit box color: '+bs3); PALETTE=[['#ff0000','red','red'],['#30343c','slate','slate']]; buildUITable(); - const mlrow=document.querySelector('#uibody tr[data-face="mode-line"]'),boxCell=mlrow&&mlrow.cells[7],boxSel=boxCell&&boxCell.querySelector('select'),boxDd=boxCell&&boxCell.querySelector('.cdd'); - if(boxSel&&boxDd){boxSel.value='line';boxSel.dispatchEvent(new Event('change',{bubbles:true}));boxDd.click();const redRow=[...document.querySelectorAll('.cddpop .cddrow')].find(r=>r.textContent.includes('red'));if(redRow)redRow.click();} + const mlrow=document.querySelector('#uibody tr[data-face="mode-line"]'),boxCell=mlrow&&mlrow.cells[7],lineBtn=boxCell&&boxCell.querySelector('.boxbtn[data-style="line"]'),boxDd=boxCell&&boxCell.querySelector('.cdd'); + if(lineBtn&&boxDd){lineBtn.click();boxDd.click();const redRow=[...document.querySelectorAll('.cddpop .cddrow')].find(r=>r.textContent.includes('red'));if(redRow)redRow.click();} A(UIMAP['mode-line'].box&&UIMAP['mode-line'].box.color==='#ff0000','UI box color dropdown writes box.color'); const app=curApp(),face=APPS[app].faces[0][0];PKGMAP[app][face].box={style:'line',width:1,color:null};buildPkgTable(); const prow=document.querySelector('#pkgbody tr[data-face="'+face+'"]'),pbox=prow&&prow.cells[8],pdd=pbox&&pbox.querySelector('.cdd'); @@ -675,3 +675,24 @@ if(location.hash==='#viewtest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c } document.title='VIEWTEST '+(ok?'PASS':'FAIL'); const d=document.createElement('div');d.id='viewtest';d.textContent='VIEWTEST '+(ok?'PASS':'FAIL')+(notes.length?' fails='+notes.join(','):'');document.body.appendChild(d);} +// Box-cluster gate (open with #boxtest): the box control is a 2x2 cluster of +// four radio buttons (none / line / pressed / raised); the color swatch shows +// only while a box style is active. +if(location.hash==='#boxtest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}}; + LOCKED.clear();const f=UI_FACES[0][0];const saveBox=UIMAP[f].box; + UIMAP[f].box=null;buildUITable(); + const cell=document.querySelector('#uibody tr[data-face="'+f+'"]').cells[7]; + A(!!cell.querySelector('.boxcluster'),'box-cluster-present'); + A(cell.querySelectorAll('.boxbtn').length===4,'four-box-buttons'); + const dd=cell.querySelector('.cstep'); + A(dd&&dd.style.display==='none','color-hidden-when-no-box'); + const lineBtn=cell.querySelector('.boxbtn[data-style="line"]');lineBtn.click(); + A(UIMAP[f].box&&UIMAP[f].box.style==='line','line-click-sets-style'); + A(lineBtn.classList.contains('on'),'line-button-active'); + A(dd.style.display!=='none','color-shown-when-box-active'); + cell.querySelector('.boxbtn[data-style=""]').click(); + A(UIMAP[f].box===null,'blank-click-clears-box'); + A(dd.style.display==='none','color-hidden-again-after-clear'); + UIMAP[f].box=saveBox;buildUITable(); + document.title='BOXTEST '+(ok?'PASS':'FAIL'); + const d=document.createElement('div');d.id='boxtest';d.textContent='BOXTEST '+(ok?'PASS':'FAIL')+(notes.length?' fails='+notes.join(','):'');document.body.appendChild(d);} diff --git a/scripts/theme-studio/styles.css b/scripts/theme-studio/styles.css index a2242728b..2c5377e7a 100644 --- a/scripts/theme-studio/styles.css +++ b/scripts/theme-studio/styles.css @@ -14,7 +14,11 @@ #pkgtable td:nth-child(3),#pkgtable td:nth-child(4){width:78px;min-width:78px;max-width:78px;padding-left:4px;padding-right:4px;text-align:center} #legtable th:nth-child(6),#legtable td:nth-child(6), #uitable th:nth-child(8),#uitable td:nth-child(8), - #pkgtable th:nth-child(9),#pkgtable td:nth-child(9){width:166px;min-width:166px;max-width:166px;padding-left:6px;padding-right:6px;text-align:left} + #pkgtable th:nth-child(9),#pkgtable td:nth-child(9){width:76px;min-width:76px;max-width:76px;padding-left:6px;padding-right:6px;text-align:left} + .boxcluster{display:grid;grid-template-columns:repeat(2,1fr);gap:2px} + .boxbtn{width:17px;height:15px;padding:0;border:1px solid #3a3a3a;border-radius:3px;background:#1f1c19;color:#cdced1;font:11px monospace;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center} + .boxbtn.on{background:#3a3320;border-color:#e8bd30;color:#e8bd30} + .boxbtn:disabled{opacity:.3;cursor:default} table.leg th:hover{color:#e8bd30} select.chip{appearance:none;border:1px solid #00000060;border-radius:5px;padding:5px 10px;font:bold 14px monospace;width:160px;cursor:pointer} .cstep{display:inline-flex;align-items:center;gap:4px} diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html index 458243b5d..fffe9863d 100644 --- a/scripts/theme-studio/theme-studio.html +++ b/scripts/theme-studio/theme-studio.html @@ -16,7 +16,11 @@ #pkgtable td:nth-child(3),#pkgtable td:nth-child(4){width:78px;min-width:78px;max-width:78px;padding-left:4px;padding-right:4px;text-align:center} #legtable th:nth-child(6),#legtable td:nth-child(6), #uitable th:nth-child(8),#uitable td:nth-child(8), - #pkgtable th:nth-child(9),#pkgtable td:nth-child(9){width:166px;min-width:166px;max-width:166px;padding-left:6px;padding-right:6px;text-align:left} + #pkgtable th:nth-child(9),#pkgtable td:nth-child(9){width:76px;min-width:76px;max-width:76px;padding-left:6px;padding-right:6px;text-align:left} + .boxcluster{display:grid;grid-template-columns:repeat(2,1fr);gap:2px} + .boxbtn{width:17px;height:15px;padding:0;border:1px solid #3a3a3a;border-radius:3px;background:#1f1c19;color:#cdced1;font:11px monospace;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center} + .boxbtn.on{background:#3a3320;border-color:#e8bd30;color:#e8bd30} + .boxbtn:disabled{opacity:.3;cursor:default} table.leg th:hover{color:#e8bd30} select.chip{appearance:none;border:1px solid #00000060;border-radius:5px;padding:5px 10px;font:bold 14px monospace;width:160px;cursor:pointer} .cstep{display:inline-flex;align-items:center;gap:4px} @@ -1854,15 +1858,25 @@ function syntaxStyle(k){const s=syntaxFace(k),fg=(k==='bg'?MAP['p']:resolveSynta return `color:${fg};${bg?'background:'+bg+';':''}font-weight:${s.bold?'bold':'normal'};font-style:${s.italic?'italic':'normal'};text-decoration:${dec.trim()||'none'}${bx?';box-shadow:'+bx:''}`;} // The per-row box control: none / line / raised / pressed plus optional line // color. get()/set() read and write the face's box object (null = no box). +// Box control: a 2x2 cluster of radio buttons for the four box styles (no box / +// line / pressed / raised), plus a compact color swatch shown only while a box +// style is active. Replaces the old wide select+swatch to reclaim column width. function mkBoxControl(get,set,opts={}){const wrap=document.createElement('div');wrap.className='boxctl'; - 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 dd=mkColorDropdown(ddList((get()&&get().color)||''),(get()&&get().color)||'',h=>{const cur=get();if(!cur)return;set(Object.assign({},cur,{color:h||null}));},{compact:!!opts.compact,defaultHex:opts.defaultHex}); - function paint(){const cur=get();s.value=cur&&cur.style?cur.style:'';dd.setValue(cur&&cur.color?cur.color:''); - const off=!cur||!cur.style||wrap.dataset.locked==='1';dd.dataset.locked=off?'1':'';dd.classList.toggle('locked',off);if(dd.syncLocked)dd.syncLocked();} - s.onchange=()=>{const cur=get();set(s.value?{style:s.value,width:cur&&cur.width||1,color:cur&&cur.color||null}:null);paint();}; - wrap.syncLocked=()=>{const locked=wrap.dataset.locked==='1';s.disabled=locked;paint();}; - wrap.append(s,dd);paint();return wrap;} + const cluster=document.createElement('div');cluster.className='boxcluster'; + const states=[['','no box',''],['line','line box','□'],['pressed','pressed','▼'],['released','raised','▲']]; + const btns={}; + states.forEach(([v,title,glyph])=>{const b=document.createElement('button');b.className='boxbtn';b.dataset.style=v;b.textContent=glyph;b.title=title; + b.onclick=()=>{const cur=get();set(v?{style:v,width:(cur&&cur.width)||1,color:(cur&&cur.color)||null}:null);paint();}; + cluster.appendChild(b);btns[v]=b;}); + const dd=mkColorDropdown(ddList((get()&&get().color)||''),(get()&&get().color)||'',h=>{const cur=get();if(!cur)return;set(Object.assign({},cur,{color:h||null}));paint();},{compact:true,defaultHex:opts.defaultHex}); + function paint(){const cur=get(),style=cur&&cur.style?cur.style:''; + for(const v in btns)btns[v].classList.toggle('on',v===style); + dd.style.display=style?'':'none';dd.setValue(cur&&cur.color?cur.color:''); + const locked=wrap.dataset.locked==='1'; + for(const v in btns)btns[v].disabled=locked; + const ddoff=locked||!style;dd.dataset.locked=ddoff?'1':'';dd.classList.toggle('locked',ddoff);if(dd.syncLocked)dd.syncLocked();} + wrap.syncLocked=()=>paint(); + wrap.append(cluster,dd);paint();return wrap;} 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 @@ -2881,8 +2895,8 @@ if(location.hash==='#beveltest'){let ok=true;const notes=[];const A=(c,n)=>{if(! A(bs3&&bs3.includes('rgb(255, 42, 42)')&&bs3.includes('rgb(143, 0, 0)'),'released style derives relief from explicit box color: '+bs3); PALETTE=[['#ff0000','red','red'],['#30343c','slate','slate']]; buildUITable(); - const mlrow=document.querySelector('#uibody tr[data-face="mode-line"]'),boxCell=mlrow&&mlrow.cells[7],boxSel=boxCell&&boxCell.querySelector('select'),boxDd=boxCell&&boxCell.querySelector('.cdd'); - if(boxSel&&boxDd){boxSel.value='line';boxSel.dispatchEvent(new Event('change',{bubbles:true}));boxDd.click();const redRow=[...document.querySelectorAll('.cddpop .cddrow')].find(r=>r.textContent.includes('red'));if(redRow)redRow.click();} + const mlrow=document.querySelector('#uibody tr[data-face="mode-line"]'),boxCell=mlrow&&mlrow.cells[7],lineBtn=boxCell&&boxCell.querySelector('.boxbtn[data-style="line"]'),boxDd=boxCell&&boxCell.querySelector('.cdd'); + if(lineBtn&&boxDd){lineBtn.click();boxDd.click();const redRow=[...document.querySelectorAll('.cddpop .cddrow')].find(r=>r.textContent.includes('red'));if(redRow)redRow.click();} A(UIMAP['mode-line'].box&&UIMAP['mode-line'].box.color==='#ff0000','UI box color dropdown writes box.color'); const app=curApp(),face=APPS[app].faces[0][0];PKGMAP[app][face].box={style:'line',width:1,color:null};buildPkgTable(); const prow=document.querySelector('#pkgbody tr[data-face="'+face+'"]'),pbox=prow&&prow.cells[8],pdd=pbox&&pbox.querySelector('.cdd'); @@ -3163,4 +3177,25 @@ if(location.hash==='#viewtest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c } document.title='VIEWTEST '+(ok?'PASS':'FAIL'); const d=document.createElement('div');d.id='viewtest';d.textContent='VIEWTEST '+(ok?'PASS':'FAIL')+(notes.length?' fails='+notes.join(','):'');document.body.appendChild(d);} +// Box-cluster gate (open with #boxtest): the box control is a 2x2 cluster of +// four radio buttons (none / line / pressed / raised); the color swatch shows +// only while a box style is active. +if(location.hash==='#boxtest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}}; + LOCKED.clear();const f=UI_FACES[0][0];const saveBox=UIMAP[f].box; + UIMAP[f].box=null;buildUITable(); + const cell=document.querySelector('#uibody tr[data-face="'+f+'"]').cells[7]; + A(!!cell.querySelector('.boxcluster'),'box-cluster-present'); + A(cell.querySelectorAll('.boxbtn').length===4,'four-box-buttons'); + const dd=cell.querySelector('.cstep'); + A(dd&&dd.style.display==='none','color-hidden-when-no-box'); + const lineBtn=cell.querySelector('.boxbtn[data-style="line"]');lineBtn.click(); + A(UIMAP[f].box&&UIMAP[f].box.style==='line','line-click-sets-style'); + A(lineBtn.classList.contains('on'),'line-button-active'); + A(dd.style.display!=='none','color-shown-when-box-active'); + cell.querySelector('.boxbtn[data-style=""]').click(); + A(UIMAP[f].box===null,'blank-click-clears-box'); + A(dd.style.display==='none','color-hidden-again-after-clear'); + UIMAP[f].box=saveBox;buildUITable(); + document.title='BOXTEST '+(ok?'PASS':'FAIL'); + const d=document.createElement('div');d.id='boxtest';d.textContent='BOXTEST '+(ok?'PASS':'FAIL')+(notes.length?' fails='+notes.join(','):'');document.body.appendChild(d);} -- cgit v1.2.3