diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-14 18:14:29 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-14 18:14:29 -0500 |
| commit | 214c16fe127e007965b21d38d0c9c24f8c995b4c (patch) | |
| tree | b4a54af6a928122c3f8af374618b07da5fc798c8 /scripts/theme-studio/browser-gates.js | |
| parent | 814f7dbf74af01e7932be7a994ecc8297e843f37 (diff) | |
| download | dotemacs-214c16fe127e007965b21d38d0c9c24f8c995b4c.tar.gz dotemacs-214c16fe127e007965b21d38d0c9c24f8c995b4c.zip | |
feat(theme-studio): palette generator and preview fidelity
Two strands land together because the generated theme-studio.html bundles every source file into one page and can't be split cleanly.
The palette generator is a preview-first panel: palette-generator-core.js plans the palette and palette-generator-ui.js draws it. Generated colors stay inspectable and tunable through the existing selector, and committing one creates a normal base column. It adds source-mode and scheme controls, a configurable accent count, and color names from color-names.json.
For preview fidelity, syntax and UI colors now resolve through the real Emacs inherit chains, so the preview matches how Emacs renders the theme. resolveSyntaxFg pins dec to ty (Emacs has no decorator face) and otherwise follows comment-delimiter to comment, doc to string, property to variable, function-call to function-name. resolveUiAttr walks mode-line-inactive to mode-line and line-number-current-line to line-number. The decorator label now reads "decorator to type" to match the type face Emacs uses for it.
Design recorded in the two theme-studio specs under docs/.
Diffstat (limited to 'scripts/theme-studio/browser-gates.js')
| -rw-r--r-- | scripts/theme-studio/browser-gates.js | 90 |
1 files changed, 87 insertions, 3 deletions
diff --git a/scripts/theme-studio/browser-gates.js b/scripts/theme-studio/browser-gates.js index 372937f18..3e5e12628 100644 --- a/scripts/theme-studio/browser-gates.js +++ b/scripts/theme-studio/browser-gates.js @@ -25,6 +25,7 @@ if(location.hash==='#selftest')pkgSelftest(); // the shared mkLockCell. (2) reset/erase batch actions update editable rows but // leave locked rows (syntax bare-kind, ui:, pkg: keys) untouched. if(location.hash==='#locktest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}}; + const cssRgb=h=>{const [r,g,b]=hex2rgb(h);return 'rgb('+r+', '+g+', '+b+')';}; LOCKED.clear();buildTable(); {const k=CATS.map(c=>c[0]).filter(k=>k!=='bg'&&k!=='p')[0]; const tr=document.querySelector('#legbody tr[data-kind="'+k+'"]'),step=tr.querySelector('.cstep'),dd=tr.querySelector('.cdd'),lb=tr.querySelector('.lockbtn'); @@ -41,6 +42,15 @@ if(location.hash==='#locktest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c const tr=document.querySelector('#uibody tr[data-face="region"]'),fg=tr.cells[2].querySelector('.cdd'),bg=tr.cells[3].querySelector('.cdd'); A(fg.classList.contains('is-default'),'compact default color button has default outline class'); A(!bg.classList.contains('is-default'),'compact assigned color button does not have default outline class');} + {setSyntaxFg('kw','');buildTable(); + const dd=document.querySelector('#legbody tr[data-kind="kw"] .cdd'); + A(dd&&dd.style.backgroundColor===cssRgb(MAP['p']),'syntax default fg swatch shows inherited fg color');} + {UIMAP['fringe'].bg=null;buildUITable(); + const dd=document.querySelector('#uibody tr[data-face="fringe"]').cells[3].querySelector('.cdd'); + A(dd&&dd.style.backgroundColor===cssRgb(MAP['bg']),'ui default bg swatch shows inherited ground color');} + {const app=curApp(),face=APPS[app].faces[0][0];PKGMAP[app][face].fg=null;PKGMAP[app][face].inherit=null;buildPkgTable(); + const dd=document.querySelector('#pkgbody tr[data-face="'+face+'"]').cells[2].querySelector('.cdd'); + A(dd&&dd.style.backgroundColor===cssRgb(MAP['p']),'package default fg swatch shows inherited/default fg color');} {PALETTE=[['#000000','bg','ground'],['#ffffff','fg','ground'],['#222222','gray-dark','gray'],['#888888','gray-mid','gray'],['#dddddd','gray-light','gray']];setSyntaxFg('bg','#000000');setSyntaxFg('p','#ffffff');setSyntaxFg('kw','#888888');LOCKED.clear();buildTable(); const tr=document.querySelector('#legbody tr[data-kind="kw"]'),btns=tr.querySelectorAll('.cstepbtn');btns[1].click(); A(MAP['kw']==='#dddddd'&&tr.querySelector('.cdd').dataset.val==='#dddddd','syntax right arrow steps to lighter color');btns[0].click(); @@ -134,6 +144,12 @@ if(location.hash==='#mocktest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c A([...document.querySelectorAll('#mockframe .fr')].some(e=>e.textContent.trim()),'fringe-indicator-present'); const mlbar=Q('[data-face="mode-line"]'); A(mlbar&&/box-shadow/.test(mlbar.getAttribute('style')||''),'mode-line-box'); + const textBox=Q('.mbuftext'),border=Q('[data-face="vertical-border"]'),mock=document.getElementById('mockframe'); + if(textBox&&border&&mock){ + const tr=textBox.getBoundingClientRect(),br=border.getBoundingClientRect(); + const ch=parseFloat(getComputedStyle(textBox).fontSize)*0.65; + A(br.left-tr.right<=ch*4.8,'vertical-border-near-text'); + }else A(false,'vertical-border-layout-elements-present'); UIMAP['line-number-current-line'].bold=true;buildMockFrame(); const curNum=Q('[data-face="line-number-current-line"]'); A(curNum&&/font-weight:\s*bold/.test(curNum.getAttribute('style')||''),'line-number-honors-weight'); @@ -151,6 +167,62 @@ if(location.hash==='#mocktest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c A(pkgBtn()&&pkgBtn().classList.contains('on')&&PKGMAP[app][face].bold===true,'pkg style button visual state turns on after rebuild'); document.title='MOCKTEST '+(ok?'PASS':'FAIL'); const d=document.createElement('div');d.id='mocktest';d.textContent='MOCKTEST '+(ok?'PASS':'FAIL')+(notes.length?' | '+notes.join(' ; '):'');document.body.appendChild(d);} +// Palette-generator gate (open with #generatortest): previewing is non-mutating, +// clicking a generated tile loads the existing selector, adding creates a normal +// singleton base column, and appending a preview column commits all span members +// under one stable column id. +if(location.hash==='#generatortest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}}; + const before=JSON.stringify(PALETTE); + A(document.getElementById('genaccents').value==='5','default accent count is 5'); + A(document.getElementById('gensource').value==='palette','default generator source is palette'); + A(document.querySelector('label:has(#genintent)').title==='what kind of candidate colors to look for','intent label hover explains the control'); + A(document.getElementById('genintent').title.includes('Pure exploration'),'intent select hover explains random'); + A([...document.getElementById('genintent').options].some(o=>o.value==='fill-hue-gaps'),'fill hue gaps intent is available'); + document.getElementById('genintent').value='fill-hue-gaps';syncGeneratorControls(); + A(document.getElementById('genintent').title.includes('underrepresented hue regions'),'fill hue gaps hover explains hue bonus'); + document.getElementById('genintent').value='near-palette';syncGeneratorControls(); + A(document.getElementById('genintent').title.includes('current palette base colors'),'intent select hover updates for selected intent'); + A(!document.getElementById('genscheme'),'legacy scheme control is removed from the generator UI'); + A(!document.getElementById('genhue'),'legacy base hue control is removed from the generator UI'); + document.getElementById('genaccents').value='3'; + document.getElementById('genintent').value='fill-gaps'; + document.getElementById('genvibe').value='warm'; + document.getElementById('gensource').value='palette'; + previewGenerator(); + A(GEN_PROPOSAL&&GEN_PROPOSAL.intent==='fill-gaps'&&GEN_PROPOSAL.vibe==='warm'&&GEN_PROPOSAL.sourceMode==='palette','intent/vibe/source feed the generator proposal'); + A(JSON.stringify(PALETTE)===before,'preview does not mutate palette'); + A(document.querySelectorAll('#genpreview .gencol').length===3,'renders requested preview columns'); + A(parseInt(getComputedStyle(document.querySelector('#genpreview .genchip')).width,10)===128,'generated candidate tile width matches palette tiles'); + A(parseInt(getComputedStyle(document.querySelector('#genpreview .genchip')).height,10)===58,'generated candidate tile height matches palette tiles'); + A([...document.querySelectorAll('#genpreview .gencol')].every(c=>c.querySelectorAll('.genchip').length===1),'preview columns show only one base tile each'); + const tile=document.querySelector('#genpreview .genchip[data-col="0"][data-member="0"]'); + A(!!tile,'base generated tile exists'); + const tileHex=tile&&tile.dataset.hex,tileName=tile&&tile.dataset.name; + if(tile)tile.click(); + A(document.getElementById('newhexstr').value===tileHex,'generated tile loads selector hex'); + A(document.getElementById('newname').value===tileName,'generated tile loads selector name'); + document.getElementById('genaccents').value='2'; + previewGenerator(); + A(document.querySelectorAll('#genpreview .gencol').length===2,'preview again replaces old preview columns'); + A(!document.querySelector('#genpreview .genchip.sel'),'preview again clears generated tile selection'); + document.getElementById('genaccents').value='3'; + previewGenerator(); + addColor(); + A(PALETTE.some(p=>p[0]===tileHex&&p[1]===tileName),'add color commits selected generated tile'); + const afterSingle=PALETTE.length; + previewGenerator(); + const append=document.querySelector('#genpreview .genappend[data-col="0"]'); + A(!!append,'append generated column button exists'); + if(append)append.click(); + A(PALETTE.length===afterSingle+1,'append commits one generated base color'); + const added=PALETTE.slice(-1); + A(new Set(added.map(p=>p[2])).size===1,'appended generated span has one stable column id'); + A(!/[+-]\d+$/.test(added[0][1]),'appended generated color is a base name, not a signed span neighbor'); + GEN_PROPOSAL={summary:{generated:1,rejected:0,minContrast:null},columns:[{name:'medium-aquamarine',members:[{name:'medium-aquamarine',hex:'#66cdaa',offset:0,columnId:'medium-aquamarine'}]}]}; + renderGeneratorPreview(); + 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);} 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();}} @@ -219,15 +291,27 @@ if(location.hash==='#contrasttest'){let ok=true;const notes=[];const A=(c,n)=>{i UIMAP['region']={fg:null,bg:'#202830',bold:false,italic:false,underline:false,strike:false}; buildUITable(); const cell=document.getElementById('uicr-region'); - A(cell&&/^worst:/.test(cell.textContent),'region shows the worst-case readout: '+(cell&&cell.textContent)); - A(cell&&cell.textContent.includes('#67809c'),'limiting fg is keyword blue: '+(cell&&cell.textContent)); - A(cell&&/\b(PASS|FAIL)\b/.test(cell.textContent),'readout carries a verdict'); + A(cell&&/^\d+\.\d (PASS|FAIL)$/.test(cell.textContent.trim()),'region shows compact worst-case readout: '+(cell&&cell.textContent)); + A(cell&&!cell.textContent.includes('#67809c'),'compact readout omits limiting fg details: '+(cell&&cell.textContent)); + A(cell&&cell.title.includes('kw (keyword) #67809c'),'hover names failing keyword blue: '+(cell&&cell.title)); + const badge=document.querySelector('#uiprev-region .crerr'); + A(badge&&badge.textContent.trim()===cell.textContent.trim(),'region preview shows failing contrast badge: '+(badge&&badge.textContent)); + A(badge&&badge.title.includes('kw (keyword) #67809c'),'preview badge hover carries failures: '+(badge&&badge.title)); + const firstFail=badge&&badge.title.split('\n')[1]; + A(firstFail&&firstFail.includes('kw (keyword) #67809c'),'failures are sorted from worst first: '+firstFail); const fl=floor('#202830',fgSetForFace('region').set); A(fl.limitingHex==='#67809c','floor limiting is blue, got '+fl.limitingHex); A(Math.abs(fl.ratio-contrast('#67809c','#202830'))<1e-9,'floor ratio matches blue-on-bg'); + UIMAP['region']={fg:'#f0fef0',bg:'#202830',bold:false,italic:false,underline:false,strike:false}; + buildUITable(); + const pairCell=document.getElementById('uicr-region'),pairWant=contrast('#f0fef0','#202830'); + A(pairCell&&Math.abs(parseFloat(pairCell.textContent)-pairWant)<0.06,'region with explicit fg rates its own fg/bg pair: got '+(pairCell&&pairCell.textContent.trim())+' want '+pairWant.toFixed(1)); + A(!document.querySelector('#uiprev-region .crerr'),'region with explicit fg does not show covered-text error badge'); + A(pairCell&&!pairCell.title.includes('#67809c'),'region explicit fg hover omits underlying syntax failures: '+(pairCell&&pairCell.title)); const ml=document.getElementById('uicr-mode-line'); A(worstCellHtml('mode-line')===null,'mode-line is out of scope (single-pair)'); A(ml&&/^\d/.test(ml.textContent.trim()),'mode-line cell is a numeric ratio: '+(ml&&ml.textContent)); + UIMAP['region']={fg:null,bg:'#202830',bold:false,italic:false,underline:false,strike:false}; setSyntaxFg('p','');CATS.forEach(c=>{if(c[0]!=='bg')setSyntaxFg(c[0],'');});buildUITable(); const empty=document.getElementById('uicr-region'); A(empty&&empty.textContent.trim()==='no fg set','empty set reads the no-set message: '+(empty&&empty.textContent)); |
