diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-13 17:17:31 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-13 17:17:31 -0500 |
| commit | d93560446f954a44890b8472f90d57c3080993df (patch) | |
| tree | da5f94a93fb296709f1c1272a2c118ffee47dac5 /scripts/theme-studio/app.js | |
| parent | c18d914c138d04157afe64a64b7cd47aaa3171b0 (diff) | |
| download | dotemacs-d93560446f954a44890b8472f90d57c3080993df.tar.gz dotemacs-d93560446f954a44890b8472f90d57c3080993df.zip | |
Refactor theme studio palette tests
Diffstat (limited to 'scripts/theme-studio/app.js')
| -rw-r--r-- | scripts/theme-studio/app.js | 649 |
1 files changed, 5 insertions, 644 deletions
diff --git a/scripts/theme-studio/app.js b/scripts/theme-studio/app.js index 25b63d56..7f674a24 100644 --- a/scripts/theme-studio/app.js +++ b/scripts/theme-studio/app.js @@ -106,13 +106,12 @@ function pkgLockKeys(){const app=curApp();return APPS[app].faces.map(f=>'pkg:'+a function tierLockKeys(tier){return tier==='syntax'?syntaxLockKeys():tier==='ui'?uiLockKeys():pkgLockKeys();} function updateLockToggle(tier){ const ids={syntax:'syntaxlocktoggle',ui:'uilocktoggle',pkg:'pkglocktoggle'},b=document.getElementById(ids[tier]);if(!b)return; - const keys=tierLockKeys(tier),all=keys.length&&keys.every(k=>LOCKED.has(k)); - b.textContent=all?'unlock all':'lock all'; + b.textContent=lockToggleLabel(tierLockKeys(tier),LOCKED); } function updateLockToggles(){updateLockToggle('syntax');updateLockToggle('ui');updateLockToggle('pkg');} function toggleAllLocks(tier){ - const keys=tierLockKeys(tier),all=keys.length&&keys.every(k=>LOCKED.has(k)); - keys.forEach(k=>all?LOCKED.delete(k):LOCKED.add(k)); + const all=areAllLocked(tierLockKeys(tier),LOCKED); + LOCKED=toggleLockSet(tierLockKeys(tier),LOCKED); if(tier==='syntax')buildTable();else if(tier==='ui')buildUITable();else buildPkgTable(); updateLockToggles(); notify((all?'unlocked ':'locked ')+(tier==='pkg'?'package':tier)+' rows',false); @@ -130,19 +129,6 @@ function clearUnlockedPkg(){ clearUnlockedRows(APPS[app].faces,f=>'pkg:'+app+':'+f[0],f=>{PKGMAP[app][f[0]]=normalizePkgFace({source:'cleared'},'cleared');}); pkgChanged();notify('cleared unlocked '+app+' faces to default',false); } -function clearPalette(){ - normalizePalette(); - const keep=[],ground={bg:MAP['bg'],fg:MAP['p']}; - PALETTE.filter(entry=>!groundRoleOfEntry(entry,ground)).forEach(([hex,name])=>{if(name)lastGone[name.toLowerCase()]=hex;}); - const addEndpoint=(role,hex,name)=>{ - const found=PALETTE.find(entry=>groundRoleOfEntry(entry,ground)===role); - keep.push(found||[hex,name,'ground']); - }; - addEndpoint('bg',MAP['bg'],'bg');addEndpoint('fg',MAP['p'],'fg'); - PALETTE=keep;selectedIdx=null; - renderPalette();buildTable();buildUITable();if(document.getElementById('pkgbody'))buildPkgTable();renderCode();applyGround(); - notify('cleared palette to bg and fg',false); -} function buildTable(){ const tb=document.getElementById('legbody');tb.innerHTML=''; for(const [kind,label,ex] of CATS){ @@ -170,211 +156,7 @@ function buildTable(){ tb.appendChild(tr);} updateLockToggle('syntax'); } -let selectedIdx=null; -// When a named palette color is deleted, remember its hex keyed by name so that -// recreating a color with the same name can re-bind the assignments still pointing -// at the old (now "(gone)") hex. Consumed once per name; cleared on import. -let lastGone={}; -// Re-point every assignment — syntax map, UI faces, package faces — from one hex -// to another. Used when a palette color's value is edited and when a deleted name -// is recreated. -function repointHex(oldHex,newHex){ - if(oldHex===newHex)return; - for(const k in MAP){if(MAP[k]===oldHex)MAP[k]=newHex;} - for(const f in UIMAP){if(UIMAP[f].fg===oldHex)UIMAP[f].fg=newHex;if(UIMAP[f].bg===oldHex)UIMAP[f].bg=newHex;} - for(const ap in PKGMAP)for(const fc in PKGMAP[ap]){const o=PKGMAP[ap][fc];if(o.fg===oldHex)o.fg=newHex;if(o.bg===oldHex)o.bg=newHex;} -} -// On adding a color, if its name matches a recently-deleted one, re-bind the -// stranded assignments to the new hex. Returns true when a heal context existed. -function healGone(name,newHex){const k=name.toLowerCase();if(!(k in lastGone))return false;const g=lastGone[k];delete lastGone[k];repointHex(g,newHex);return true;} -function normalizePaletteEntry(entry){ - const hex=entry&&entry[0],name=(entry&&entry[1])||'color'; - return [hex,name,(entry&&entry[2])||columnIdOf(entry)]; -} -function normalizePalette(){PALETTE=PALETTE.map(normalizePaletteEntry);} -// The ground column is explicit: bg pins the top endpoint, fg pins the bottom -// endpoint, and generated ground-N steps live between them. -function groundColumnMembers(){ - const ground={bg:MAP['bg'],fg:MAP['p']}; - const byRole={bg:null,fg:null,steps:[]}; - for(const entry of PALETTE){ - const role=groundRoleOfEntry(entry,ground); - if(role==='bg'||role==='fg')byRole[role]={hex:entry[0],name:entry[1]}; - else if(role==='step')byRole.steps.push({hex:entry[0],name:entry[1]}); - } - const stepIndex=m=>{const x=(m.name||'').match(/^ground-(\d+)$/i);return x?parseInt(x[1],10):Infinity;}; - byRole.steps.sort((a,b)=>stepIndex(a)-stepIndex(b)); - return [byRole.bg||{hex:MAP['bg'],name:'bg'},...byRole.steps,byRole.fg||{hex:MAP['p'],name:'fg'}]; -} -function groundSpanCount(){return PALETTE.filter(entry=>groundRoleOfEntry(entry,{bg:MAP['bg'],fg:MAP['p']})==='step').length;} -function groundSpanControl(){ - const d=document.createElement('div');d.className='fcount'; - d.innerHTML=`<span title="number of ground colors between bg and fg">span <input type="number" min="0" max="8" value="${groundSpanCount()}"></span>`; - d.querySelector('input').onchange=(e)=>setGroundSpan(Math.max(0,Math.min(8,parseInt(e.target.value,10)||0))); - return d; -} -function setGroundSpan(n){ - const old=PALETTE.filter(entry=>groundRoleOfEntry(entry,{bg:MAP['bg'],fg:MAP['p']})==='step'); - const bg=srgb2oklab(MAP['bg']),fg=srgb2oklab(MAP['p']); - const entries=[]; - for(let i=1;i<=n;i++){ - const t=i/(n+1); - const lab={L:bg.L+(fg.L-bg.L)*t,a:bg.a+(fg.a-bg.a)*t,b:bg.b+(fg.b-bg.b)*t}; - entries.push([lrgb2hex(oklab2lrgb(lab.L,lab.a,lab.b)),'ground-'+i,'ground']); - } - for(const [oldHex,oldName] of old){ - const next=entries.find(([,name])=>name===oldName); - if(next&&next[0].toLowerCase()!==oldHex.toLowerCase())repointHex(oldHex,next[0]); - } - for(let i=PALETTE.length-1;i>=0;i--)if(groundRoleOfEntry(PALETTE[i],{bg:MAP['bg'],fg:MAP['p']})==='step')PALETTE.splice(i,1); - let at=PALETTE.findIndex(entry=>groundRoleOfEntry(entry,{bg:MAP['bg'],fg:MAP['p']})==='bg'); - if(at<0)at=0; else at+=1; - PALETTE.splice(Math.min(at,PALETTE.length),0,...entries); - selectedIdx=null;renderPalette();buildTable();buildUITable();renderCode();applyGround(); - notify('set ground span to '+n,false); -} -// Pairwise OKLab ΔE over the palette. Returns the sub-threshold pairs (sorted -// closest-first) and each color's nearest-neighbor distance for its chip title. -// Pure pairwise ΔE analysis lives in colormath.js (paletteWarnings); this renders it. -function renderPaletteWarnings(warnings,overflow){ - const w=document.getElementById('palwarn');if(!w)return; - if(!warnings.length){w.style.display='none';w.innerHTML='';return;} - let html='<div class="pwh">too-similar colors</div>'; - html+=warnings.map(p=>`<div class="pwl">${esc(p.aName+' / '+p.bName)} — \u0394E ${p.dE.toFixed(3)}, hard to distinguish</div>`).join(''); - if(overflow>0)html+=`<div class="pwl">and ${overflow} more</div>`; - w.innerHTML=html;w.style.display='block'; -} -// One palette chip for PALETTE[i], with its remove / rename / select handlers. -// Families sort deterministically, so the old move-arrow / drag reordering is gone. -function paletteChip(i,nearest){ - const [hex,name]=PALETTE[i],tc=textOn(hex),nde=nearest[i]; - const role=groundRoleOfEntry(PALETTE[i],{bg:MAP['bg'],fg:MAP['p']}); - const locked=(role==='bg'||role==='fg'); - const d=document.createElement('div');d.className='pchip'+(i===selectedIdx?' sel':'');d.style.background=hex; - d.title=name+' '+hex+(nde===Infinity||nde===undefined?'':' — nearest ΔE '+nde.toFixed(3)); - const rm=locked?`<span class="lock" title="${role==='bg'?'background':'foreground'} — can't remove" style="color:${tc}">🔒</span>`:`<button class="rm" title="remove" style="color:${tc}">×</button>`; - d.innerHTML=`${rm}<input class="nm" value="${name}" style="color:${tc}"><div class="hx" style="color:${tc}">${hex}</div>`; - if(!locked)d.querySelector('.rm').onclick=(e)=>{e.stopPropagation();if(name)lastGone[name.toLowerCase()]=hex;PALETTE.splice(i,1);if(selectedIdx===i)selectedIdx=null;renderPalette();buildTable();buildUITable();}; - d.querySelector('.nm').onchange=(e)=>{PALETTE[i][1]=e.target.value;buildTable();buildUITable();}; - d.onclick=(e)=>{if(e.target.closest('.rm')||e.target.closest('.nm'))return;selectColor(i);}; - return d; -} -function paletteIndexByHexName(hex,name){ - for(let i=0;i<PALETTE.length;i++)if(PALETTE[i][0]===hex&&PALETTE[i][1]===name)return i; - return -1; -} -function selectColumnBase(f){ - const baseMember=f.members.find(m=>m.hex.toLowerCase()===f.base.toLowerCase())||f.members[0]; - const i=paletteIndexByHexName(baseMember.hex,baseMember.name); - if(i>=0)selectColor(i); -} -function isGroundEntry(entry){ - return !!groundRoleOfEntry(entry,{bg:MAP['bg'],fg:MAP['p']}); -} -function moveColumn(columnId,dir){ - normalizePalette(); - const columns=sortColumns(columnsFromPalette(PALETTE,{bg:MAP['bg'],fg:MAP['p']}).columns); - const pos=columns.findIndex(f=>f.column===columnId); - const next=columns[pos+dir]; - if(pos<0||!next)return; - const moving=[],rest=[]; - PALETTE.forEach(entry=>{ - if(!isGroundEntry(entry)&&columnIdOf(entry)===columnId)moving.push(entry); - else rest.push(entry); - }); - const nextPositions=[]; - rest.forEach((entry,i)=>{if(!isGroundEntry(entry)&&columnIdOf(entry)===next.column)nextPositions.push(i);}); - if(!nextPositions.length)return; - const at=dir<0?nextPositions[0]:nextPositions[nextPositions.length-1]+1; - PALETTE=rest.slice(0,at).concat(moving,rest.slice(at)); - selectedIdx=null;renderPalette();buildTable();buildUITable();renderCode();applyGround(); - notify('moved "'+columnId+'" '+(dir<0?'left':'right'),false); -} -function columnHeader(f,position,count){ - const h=document.createElement('div');h.className='fhead'; - const label=(f.members.find(m=>m.hex.toLowerCase()===f.base.toLowerCase())||{}).name||f.column||f.base; - h.innerHTML=`<button class="cmove left" title="move column left" ${position===0?'disabled':''}>‹</button><button class="ctitle" title="select base color"></button><button class="cmove right" title="move column right" ${position===count-1?'disabled':''}>›</button>`; - h.querySelector('.ctitle').textContent=label; - h.querySelector('.ctitle').onclick=()=>selectColumnBase(f); - h.querySelector('.left').onclick=(e)=>{e.stopPropagation();moveColumn(f.column,-1);}; - h.querySelector('.right').onclick=(e)=>{e.stopPropagation();moveColumn(f.column,1);}; - return h; -} -// Render the palette as structural color columns: pinned ground column, then -// first-seen palette columns. Grouping uses the stable column id stored on each -// palette entry, so renaming a color never moves it. -function renderPalette(){ - normalizePalette(); - const p=document.getElementById('pals');p.innerHTML=''; - const {warnings,overflow,nearest}=paletteWarnings(PALETTE,DELTAE_MIN,5); - const {ground,columns}=columnsFromPalette(PALETTE,{bg:MAP['bg'],fg:MAP['p']}); - const used=new Set(); - const idxOf=(hex,name)=>{for(let i=0;i<PALETTE.length;i++)if(!used.has(i)&&PALETTE[i][0]===hex&&PALETTE[i][1]===name){used.add(i);return i;}return -1;}; - const strip=(cls)=>{const s=document.createElement('div');s.className='fstrip'+(cls||'');p.appendChild(s);return s;}; - if(ground.length){ - const gs=strip(' ground');gs.dataset.column='ground'; - const gh=document.createElement('div');gh.className='fhead';gh.textContent='ground';gs.appendChild(gh); - gs.appendChild(groundSpanControl()); - groundColumnMembers().forEach(m=>{ - const i=idxOf(m.hex,m.name); - if(i>=0)gs.appendChild(paletteChip(i,nearest)); - else{const tc=textOn(m.hex),sw=document.createElement('div');sw.className='pchip';sw.style.background=m.hex;sw.title=(m.name||'ground')+' '+m.hex; - sw.innerHTML=`<input class="nm" value="${m.name||'ground'}" disabled style="color:${tc}"><div class="hx" style="color:${tc}">${m.hex}</div>`;gs.appendChild(sw);} - }); - } - // The too-similar warning stays on the full flat palette: a generated ramp's - // steps are a stepL apart (well above the warning's ΔE threshold), so they never - // trigger it, and any pair that does is a genuine near-duplicate worth flagging. - const ordered=sortColumns(columns); - ordered.forEach((f,pos)=>{ - const s=strip('');s.dataset.column=f.column||f.base; - s.appendChild(columnHeader(f,pos,ordered.length)); - s.appendChild(columnCountControl(f)); - f.members.forEach(m=>{const i=idxOf(m.hex,m.name);if(i>=0)s.appendChild(paletteChip(i,nearest));}); - }); - renderPaletteWarnings(warnings,overflow); - buildUITable();if(document.getElementById('pkgbody'))buildPkgTable(); -} -// The per-column count control under a chromatic strip. Its value is the column's -// current per-side reach; setting N regenerates the column as base ±N. -function columnCountControl(f){ - const per=Math.max(0,...rankByLightness(f.members.map(m=>m.hex),f.base).map(m=>Math.abs(m.offset))); - const d=document.createElement('div');d.className='fcount'; - d.innerHTML=`<span title="set the column span: N generated steps on each side of the base — this replaces the column">span ± <input type="number" min="0" max="4" value="${per}"></span>`; - d.querySelector('input').onchange=(e)=>setColumnCount(f.base,Math.max(0,Math.min(4,parseInt(e.target.value,10)||0))); - return d; -} -// Regenerate a column as a symmetric base ±N ramp, replacing its current members. -// References to a surviving position (matched by signed lightness rank) follow the -// new hex; references to a position removed by lowering N leave their old hex, -// which is no longer in the palette and so renders as "(gone)". -// Replace oldHexes in the palette with a fresh base ±n ramp, repointing surviving -// references and leaving removed ones on their now-gone hex. Returns the removed -// count, or null on a bad base. Shared by the count control and the base edit. -function regenColumnInPlace(oldHexes,baseHex,baseName,n,columnId){ - const r=regenColumn(baseHex,n,{}); - if(r.error){notify('cannot regenerate from '+baseHex,true);return null;} - const plan=stepRepointPlan(rankByLightness(oldHexes,baseHex),r.members); - const oldSet=new Set(oldHexes.map(h=>h.toLowerCase())); - let at=PALETTE.length; - for(let i=0;i<PALETTE.length;i++)if(oldSet.has(PALETTE[i][0].toLowerCase())){at=i;break;} - for(let i=PALETTE.length-1;i>=0;i--)if(oldSet.has(PALETTE[i][0].toLowerCase()))PALETTE.splice(i,1); - const col=columnId||columnStem(baseName); - const entries=r.members.map(m=>[m.hex,m.offset===0?baseName:baseName+(m.offset>0?'+'+m.offset:String(m.offset)),col]); - PALETTE.splice(Math.min(at,PALETTE.length),0,...entries); - for(const [o,nw] of plan.map)repointHex(o,nw); - return plan.removed.length; -} -function setColumnCount(baseHex,n){ - const {columns}=columnsFromPalette(PALETTE,{bg:MAP['bg'],fg:MAP['p']}); - const column=columns.find(f=>f.base.toLowerCase()===baseHex.toLowerCase()); - if(!column)return; - const baseName=(column.members.find(m=>m.hex.toLowerCase()===baseHex.toLowerCase())||{}).name||'color'; - const removed=regenColumnInPlace(column.members.map(m=>m.hex),baseHex,baseName,n,column.column); - if(removed===null)return; - selectedIdx=null;renderPalette();buildTable();buildUITable();renderCode();applyGround(); - notify('regenerated "'+baseName+'" to ±'+n+(removed?(' — '+removed+' removed step(s) show "(gone)" where used'):''),false); -} +PALETTE_ACTIONS_J function notify(msg,err){const m=document.getElementById('palmsg');if(!m)return;m.textContent=msg;m.style.color=err?'#cb6b4d':'#8a9496';m.style.opacity='1';clearTimeout(m._t);m._t=setTimeout(()=>{m.style.opacity='0';},err?4000:2800);} function applyEdit(){if(selectedIdx!==null)updateColor();else addColor();} function selectColor(i){selectedIdx=i;const [hex,name]=PALETTE[i];setHex(hex);document.getElementById('newname').value=name;renderPalette();notify('editing "'+name+'" — change the value, then Enter (or Update selected) to save',false);} @@ -1034,425 +816,4 @@ function srtTable(tbId,col){tableSort[tbId]={col,asc:!(tableSort[tbId]&&tableSor function applyTableSort(tbId){const s=tableSort[tbId];if(!s)return;const tb=document.getElementById(tbId);if(!tb)return;const dir=s.asc?1:-1;const r=[...tb.rows];r.sort((a,b)=>{const x=cellVal(a.cells[s.col]),y=cellVal(b.cells[s.col]);return ((typeof x==='number'&&typeof y==='number')?x-y:(x<y?-1:x>y?1:0))*dir;});r.forEach(x=>tb.appendChild(x));} buildLangSel();buildAppSel();renderPalette();buildTable();buildUITable();renderCode();applyGround();updateTitle();initPicker();buildPkgTable();buildPkgPreview();syncMockHeight();syncPkgHeight(); addEventListener('resize',()=>{syncMockHeight();syncPkgHeight();}); -// Phase-1 self-test (open with #selftest): seed -> export -> import -> compare. -function pkgSelftest(){ - const seeded=seedPkgmap(); - seeded['org-mode']['org-level-2']={fg:'#e8bd30',bg:null,bold:false,italic:false,inherit:'org-level-1',height:1.2,source:'user'}; - const exp=packagesForExport(seeded); - const round=seedPkgmap();mergePackagesInto(round,exp); - const roundtrip=JSON.stringify(exp)===JSON.stringify(packagesForExport(round)); - let oldjson=true;try{const m=seedPkgmap();mergePackagesInto(m,undefined);oldjson=!!(m['org-mode']&&m['org-mode']['org-todo'].source==='default');}catch(e){oldjson=false;} - const l2=exp['org-mode']['org-level-2']; - const inherited=l2.inherit==='org-level-1'&&l2.source==='user'; - const height=l2.height===1.2 && !('height' in (exp['org-mode']['org-todo'])); - const sc=seedPkgmap();sc['org-mode']['org-todo']={fg:null,bg:null,bold:false,italic:false,inherit:null,height:1,source:'cleared'}; - const cleared='org-todo' in packagesForExport(sc)['org-mode']; - const su=seedPkgmap();mergePackagesInto(su,{'zzz-pkg':{'zzz-face':{fg:'#112233',source:'user'}}}); - const unknown=!!(su['zzz-pkg']&&su['zzz-pkg']['zzz-face'].fg==='#112233'); - PKGMAP['__cyc']={a:{fg:null,bg:null,bold:false,italic:false,inherit:'b',height:1,source:'user'},b:{fg:null,bg:null,bold:false,italic:false,inherit:'a',height:1,source:'user'}}; - let cyc=true;try{pkgEffFg('__cyc','a');}catch(e){cyc=false;}delete PKGMAP['__cyc']; - const verdict=(roundtrip&&oldjson&&inherited&&height&&cleared&&unknown&&cyc)?'PASS':'FAIL'; - document.title='SELFTEST '+verdict; - const d=document.createElement('div');d.id='selftest';d.textContent='SELFTEST '+verdict+' roundtrip='+roundtrip+' oldjson='+oldjson+' inherit='+inherited+' height='+height+' cleared='+cleared+' unknown='+unknown+' cycle='+cyc;document.body.appendChild(d); -} -if(location.hash==='#selftest')pkgSelftest(); -// Lock-mechanism gate (open with #locktest): two behaviors the refactor must -// preserve, across all three tiers. (1) Locking a row disables its control via -// the shared mkLockCell — syntax uses a swatch div (data-locked), UI a native -// select (.disabled). (2) clear-unlocked wipes unlocked rows to default but -// leaves 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);}}; - 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+'"]'),dd=tr.querySelector('.cdd'),lb=tr.querySelector('.lockbtn'); - A(dd.dataset.locked!=='1','syntax-dd-starts-unlocked');lb.click(); - A(dd.dataset.locked==='1'&&dd.classList.contains('locked'),'syntax-lock-disables-dd');lb.click(); - A(dd.dataset.locked!=='1','syntax-unlock-reenables-dd');} - LOCKED.clear();buildUITable(); - {const f=UI_FACES[0][0]; - const tr=document.querySelector('#uibody tr[data-face="'+f+'"]'),dd=tr.querySelector('.cdd'),lb=tr.querySelector('.lockbtn'); - A(dd.dataset.locked!=='1','ui-dd-starts-unlocked');lb.click(); - A(dd.dataset.locked==='1'&&dd.classList.contains('locked'),'ui-lock-disables-dd');lb.click(); - A(dd.dataset.locked!=='1','ui-unlock-reenables-dd');} - {const ks=CATS.map(c=>c[0]).filter(k=>k!=='bg'&&k!=='p'),k1=ks[0],k2=ks[1]; - MAP[k1]='#111111';MAP[k2]='#222222';LOCKED.clear();LOCKED.add(k1);clearUnlocked(); - A(MAP[k1]==='#111111','syntax-clear-keeps-locked');A(MAP[k2]==='','syntax-clear-wipes-unlocked');} - {const f1=UI_FACES[0][0],f2=UI_FACES[1][0]; - UIMAP[f1].fg='#111111';UIMAP[f2].fg='#222222';LOCKED.clear();LOCKED.add('ui:'+f1);clearUnlockedUI(); - A(UIMAP[f1].fg==='#111111','ui-clear-keeps-locked');A(UIMAP[f2].fg===null,'ui-clear-wipes-unlocked');} - {const app=curApp(),pf=APPS[app].faces.map(r=>r[0]),p1=pf[0],p2=pf[1]; - PKGMAP[app][p1].fg='#111111';PKGMAP[app][p2].fg='#222222';LOCKED.clear();LOCKED.add('pkg:'+app+':'+p1);clearUnlockedPkg(); - A(PKGMAP[app][p1].fg==='#111111','pkg-clear-keeps-locked');A(PKGMAP[app][p2].fg===null,'pkg-clear-wipes-unlocked');} - {LOCKED.clear();buildTable();const b=document.getElementById('syntaxlocktoggle');A(b&&b.textContent==='lock all','syntax toggle starts as lock all');b.click(); - A(syntaxLockKeys().every(k=>LOCKED.has(k))&&b.textContent==='unlock all','syntax lock-all locks every syntax row and flips label');b.click(); - A(syntaxLockKeys().every(k=>!LOCKED.has(k))&&b.textContent==='lock all','syntax unlock-all clears every syntax lock and flips label');} - {LOCKED.clear();buildUITable();const b=document.getElementById('uilocktoggle');A(b&&b.textContent==='lock all','ui toggle starts as lock all');b.click(); - A(uiLockKeys().every(k=>LOCKED.has(k))&&b.textContent==='unlock all','ui lock-all locks every UI row and flips label');b.click(); - A(uiLockKeys().every(k=>!LOCKED.has(k))&&b.textContent==='lock all','ui unlock-all clears every UI lock and flips label');} - {LOCKED.clear();buildPkgTable();const b=document.getElementById('pkglocktoggle');A(b&&b.textContent==='lock all','pkg toggle starts as lock all');b.click(); - A(pkgLockKeys().every(k=>LOCKED.has(k))&&b.textContent==='unlock all','pkg lock-all locks every current package row and flips label');b.click(); - A(pkgLockKeys().every(k=>!LOCKED.has(k))&&b.textContent==='lock all','pkg unlock-all clears every current package lock and flips label');} - document.title='LOCKTEST '+(ok?'PASS':'FAIL'); - const d=document.createElement('div');d.id='locktest';d.textContent='LOCKTEST '+(ok?'PASS':'FAIL')+(notes.length?' | '+notes.join(' ; '):'');document.body.appendChild(d);} -// Sort gate (open with #sorttest): all three tables now share srtTable/cellVal. -// Verifies the syntax table (which used to have its own srt) sorts by color -// value and by element name, that a repeat click reverses, and that the UI and -// package tables still sort. Guards the unified sort for the later stages. -if(location.hash==='#sorttest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}}; - const ddVals=tb=>[...document.querySelectorAll('#'+tb+' tr')].map(tr=>{const dd=tr.cells[2].querySelector('.cdd');return dd?(dd.dataset.val||''):'';}); - const txtVals=tb=>[...document.querySelectorAll('#'+tb+' tr')].map(tr=>tr.cells[0].innerText.trim().toLowerCase()); - const asc=a=>a.every((v,i)=>i===0||a[i-1]<=v),desc=a=>a.every((v,i)=>i===0||a[i-1]>=v); - buildTable(); - srtTable('legbody',2);A(asc(ddVals('legbody')),'legbody-color-asc'); - srtTable('legbody',2);A(desc(ddVals('legbody')),'legbody-color-desc'); - srtTable('legbody',0);A(asc(txtVals('legbody')),'legbody-elements-asc'); - buildUITable();srtTable('uibody',0);A(asc(txtVals('uibody')),'uibody-face-asc'); - buildPkgTable();srtTable('pkgbody',2);A(asc(ddVals('pkgbody')),'pkgbody-fg-asc'); - document.title='SORTTEST '+(ok?'PASS':'FAIL'); - const d=document.createElement('div');d.id='sorttest';d.textContent='SORTTEST '+(ok?'PASS':'FAIL')+(notes.length?' | '+notes.join(' ; '):'');document.body.appendChild(d);} -// Live-buffer rendering gate (open with #mocktest): pins the face-faithfulness -// fixes so they cannot silently regress — overlay faces keep syntax colors and -// honor their styles, the cursor sits on a glyph, line numbers honor weight, the -// fringe shows its foreground indicator, and the mode-line carries its box. -if(location.hash==='#mocktest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}}; - const Q=s=>document.querySelector('#mockframe '+s); - buildMockFrame(); - A(Q('[data-face="highlight"] [data-k]'),'highlight-keeps-token-colors'); - A(Q('[data-face="region"] [data-k]'),'region-keeps-token-colors'); - const curCell=Q('[data-face="cursor"]'); - A(curCell&&curCell.textContent.trim().length===1,'cursor-on-glyph'); - const laz=Q('[data-face="lazy-highlight"]'); - A(laz&&/background:\s*(?!transparent)/.test(laz.getAttribute('style')||''),'overlay-honors-background-style'); - 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'); - 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'); - UIMAP['region'].bold=false;buildUITable(); - const uiBold=[...document.querySelectorAll('#uibody tr')].find(r=>r.dataset.face==='region').querySelector('.sbtn[title="bold"]'); - A(uiBold&&!uiBold.classList.contains('on'),'ui style button starts off when model is false'); - uiBold.click(); - A(uiBold.classList.contains('on')&&UIMAP['region'].bold===true,'ui style button visual state turns on with model'); - uiBold.click(); - A(!uiBold.classList.contains('on')&&UIMAP['region'].bold===false,'ui style button visual state turns off with model'); - const app=curApp(),face=APPS[app].faces[0][0];PKGMAP[app][face].bold=false;buildPkgTable(); - const pkgBtn=()=>document.querySelector('#pkgbody tr[data-face="'+face+'"] .sbtn[title="bold"]'); - A(pkgBtn()&&!pkgBtn().classList.contains('on'),'pkg style button starts off when model is false'); - pkgBtn().click(); - 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);} -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();}} -if(location.hash==='#planetest'){let ok=true;const notes=[]; - document.getElementById('newhexstr').value='#67809c';openPicker();setPkModel('oklch');paintPicker(); - const sv=document.getElementById('sv'),cv=document.getElementById('svmask'),ctx=cv.getContext('2d'); - const [L,C,H]=readOklch(); - const expLeft=Math.min(1,C/OKLCH_CMAX)*sv.clientWidth,expTop=(1-L)*sv.clientHeight; - const gotLeft=parseFloat(document.getElementById('svcur').style.left),gotTop=parseFloat(document.getElementById('svcur').style.top); - if(Math.abs(gotLeft-expLeft)>2||Math.abs(gotTop-expTop)>2){ok=false;notes.push('crosshair off got '+gotLeft.toFixed(1)+','+gotTop.toFixed(1)+' exp '+expLeft.toFixed(1)+','+expTop.toFixed(1));} - const Coog=0.38,Loog=0.5,labO=oklch2oklab(Loog,Coog,H),oog=!inGamut(oklab2lrgb(labO.L,labO.a,labO.b)); - const oogX=Math.min(cv.width-2,Math.round((Coog/OKLCH_CMAX)*cv.width)),oogY=Math.round((1-Loog)*cv.height); - const dO=ctx.getImageData(oogX,oogY,1,1).data,greyO=Math.abs(dO[0]-0x15)<10&&Math.abs(dO[1]-0x12)<10&&Math.abs(dO[2]-0x0f)<10; - if(oog&&!greyO){ok=false;notes.push('OOG cell not masked rgb '+dO[0]+','+dO[1]+','+dO[2]);} - const inX=Math.round((0.03/OKLCH_CMAX)*cv.width),inY=Math.round(0.5*cv.height); - const dI=ctx.getImageData(inX,inY,1,1).data,greyI=Math.abs(dI[0]-0x15)<10&&Math.abs(dI[1]-0x12)<10&&Math.abs(dI[2]-0x0f)<10; - if(greyI){ok=false;notes.push('in-gamut cell rendered as OOG grey rgb '+dI[0]+','+dI[1]+','+dI[2]);} - document.title='PLANETEST '+(ok?'PASS':'FAIL');const d=document.createElement('div');d.id='planetest';d.textContent='PLANETEST '+(ok?'PASS':'FAIL')+(notes.length?' | '+notes.join(' ; '):'');document.body.appendChild(d);} -if(location.hash==='#oklchtest'){let ok=true;const notes=[]; - document.getElementById('newhexstr').value='#67809c';openPicker(); - const before=document.getElementById('newhexstr').value; - setPkModel('oklch'); - if(pkModel!=='oklch'){ok=false;notes.push('model not oklch');} - if(!document.getElementById('oklchctl').classList.contains('show')){ok=false;notes.push('oklch dials hidden');} - if(document.getElementById('newhexstr').value!==before){ok=false;notes.push('color changed on model switch: '+document.getElementById('newhexstr').value);} - pkMode='any';document.querySelector('.pmode button[data-m="aa"]').click(); - if(pkModel!=='oklch'){ok=false;notes.push('mask toggle reset model');} - if(pkMode!=='aa'){ok=false;notes.push('mask did not set aa');} - setPkModel('hsv'); - if(pkMode!=='aa'){ok=false;notes.push('model switch reset mask to '+pkMode);} - if(pkModel!=='hsv'){ok=false;notes.push('model not hsv after switch');} - setPkModel('oklch');setOklchInputs(0.591,0.052,251.6);pkOklchSet(); - const driven=document.getElementById('newhexstr').value,dl=oklab2oklch(srgb2oklab(driven)); - if(!(Math.abs(dl.L-0.591)<0.02&&Math.abs(dl.C-0.052)<0.02)){ok=false;notes.push('dials did not drive color: '+driven);} - const {clamped}=oklch2hex(0.7,0.4,140);setOklchInputs(0.7,0.4,140);pkOklchSet(); - if(!(clamped&&document.getElementById('pkclamp').classList.contains('show'))){ok=false;notes.push('clamp status missing for out-of-gamut C');} - document.title='OKLCHTEST '+(ok?'PASS':'FAIL');const d=document.createElement('div');d.id='oklchtest';d.textContent='OKLCHTEST '+(ok?'PASS':'FAIL')+(notes.length?' | '+notes.join(' ; '):'');document.body.appendChild(d);} -if(location.hash==='#deltatest'){const save=PALETTE.slice();let ok=true;const notes=[];const W=()=>document.getElementById('palwarn'); - PALETTE=[['#0d0b0a','ground'],['#cdced1','fg'],['#67809c','blue'],['#69829e','blue2']];renderPalette(); - const t1=W().textContent;if(!(W().style.display!=='none'&&/blue \/ blue2/.test(t1)&&/ΔE/.test(t1))){ok=false;notes.push('near-pair did not fire: '+t1);} - PALETTE=[['#0d0b0a','ground'],['#cdced1','fg'],['#67809c','blue'],['#e8bd30','gold'],['#cb6b4d','terra']];renderPalette(); - if(W().style.display!=='none'){ok=false;notes.push('spread palette warned: '+W().textContent);} - PALETTE=[['#0d0b0a','ground'],['#cdced1','fg']];for(let k=0;k<7;k++){const v=(0x67+k).toString(16).padStart(2,'0');PALETTE.push(['#'+v+'809c','c'+k]);}renderPalette(); - const tc=W().textContent;const nums=[...tc.matchAll(/ΔE (\d+\.\d+)/g)].map(m=>parseFloat(m[1])); - if(!/and \d+ more/.test(tc)){ok=false;notes.push('no cap suffix: '+tc);} - if(!(nums.length===5&&nums.every((n,k)=>k===0||n>=nums[k-1]))){ok=false;notes.push('not 5-capped ascending: '+nums.join(','));} - PALETTE=save;renderPalette(); - document.title='DELTATEST '+(ok?'PASS':'FAIL');const d=document.createElement('div');d.id='deltatest';d.textContent='DELTATEST '+(ok?'PASS':'FAIL')+(notes.length?' | '+notes.join(' ; '):'');document.body.appendChild(d);} -if(location.hash==='#readouttest'){const hex='#67809c';document.getElementById('newhexstr').value=hex;openPicker();pkReadout(hex); - const o=document.getElementById('pkoklch').textContent,a=document.getElementById('pkapca').textContent,w=document.getElementById('pkcon').textContent; - const lch=oklab2oklch(srgb2oklab(hex)); - const expO='OKLCH '+lch.L.toFixed(3)+' '+lch.C.toFixed(3)+' '+Math.round(lch.H)+'\u00b0'; - const expA='APCA Lc '+apca(hex,MAP['bg']).toFixed(0); - const r=contrast(hex,MAP['bg']),expW=r.toFixed(1)+' '+rating(r); - const wired=o===expO&&a===expA&&w===expW; - const sane=Math.abs(lch.L-0.591)<0.01&&Math.abs(lch.C-0.052)<0.01&&Math.abs(lch.H-251.6)<2; - const ok=wired&&sane;document.title='READOUTTEST '+(ok?'PASS':'FAIL'); - const d=document.createElement('div');d.id='readouttest';d.textContent='READOUTTEST '+(ok?'PASS':'FAIL')+' oklch='+o+' | apca='+a+' | wcag='+w;document.body.appendChild(d);} -// Worst-case readout gate (open with #contrasttest): a covered overlay face shows -// the floor over its foreground set and names the limiting foreground, an -// out-of-scope face keeps the single-pair readout, and an empty set reads "no fg set". -if(location.hash==='#contrasttest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}}; - const saveMAP=Object.assign({},MAP),saveUI=JSON.parse(JSON.stringify(UIMAP)); - CATS.forEach(c=>{if(c[0]!=='bg'&&c[0]!=='p')MAP[c[0]]='';}); - MAP['p']='#f0fef0';MAP['kw']='#67809c';MAP['str']='#a3b18a';MAP['bg']='#000000'; - 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'); - 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'); - 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)); - MAP['p']='';CATS.forEach(c=>{if(c[0]!=='bg')MAP[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)); - // A two-color face (own fg AND own bg) rates its own pair, never the ground bg. - UIMAP['mode-line']={fg:'#112233',bg:'#aabbcc',bold:false,italic:false,underline:false,strike:false}; - buildUITable(); - const two=document.getElementById('uicr-mode-line'),twoWant=contrast('#112233','#aabbcc'); - A(two&&Math.abs(parseFloat(two.textContent)-twoWant)<0.06,'ui two-color face rates own fg-on-bg: got '+(two&&two.textContent.trim())+' want '+twoWant.toFixed(1)); - const tApp=Object.keys(APPS)[0],tFace=APPS[tApp].faces[0][0],savePF=JSON.parse(JSON.stringify(PKGMAP[tApp][tFace])); - Object.assign(PKGMAP[tApp][tFace],{fg:'#112233',bg:'#aabbcc',inherit:null});buildPkgTable(); - const prow=document.querySelector('#pkgbody tr[data-face="'+tFace+'"]'),pcell=prow&&prow.children[5]; - A(pcell&&Math.abs(parseFloat(pcell.textContent)-twoWant)<0.06,'pkg two-color face rates own fg-on-bg: got '+(pcell&&pcell.textContent.trim())+' want '+twoWant.toFixed(1)); - PKGMAP[tApp][tFace]=savePF;buildPkgTable(); - // A ground-bg change must not clobber a face's own preview bg, must leave a - // two-color ratio alone, and must re-rate a ground-dependent face's cell. - UIMAP['fringe']={fg:'#ddeeff',bg:null,bold:false,italic:false,underline:false,strike:false}; - buildUITable(); - MAP['bg']='#440000';applyGround(); - const pv=document.getElementById('uiprev-mode-line'); - A(pv&&pv.style.background==='rgb(170, 187, 204)','ground change keeps a face own preview bg: got '+(pv&&pv.style.background)); - const twoAfter=document.getElementById('uicr-mode-line'); - A(twoAfter&&Math.abs(parseFloat(twoAfter.textContent)-twoWant)<0.06,'ground change leaves a two-color ratio alone: got '+(twoAfter&&twoAfter.textContent.trim())); - const frc=document.getElementById('uicr-fringe'),frWant=contrast('#ddeeff','#440000'); - A(frc&&Math.abs(parseFloat(frc.textContent)-frWant)<0.06,'ground change re-rates a ground-dependent face: got '+(frc&&frc.textContent.trim())+' want '+frWant.toFixed(1)); - // A default-fg (p) change through the real syntax dropdown re-rates a face - // whose fg falls back to it. Drives the DOM so the handler wiring is pinned. - UIMAP['fringe']={fg:null,bg:'#aabbcc',bold:false,italic:false,underline:false,strike:false}; - buildUITable(); - const pLocked=LOCKED.has('p');if(pLocked){LOCKED.delete('p');buildTable();} - const pdd=document.querySelector('#legbody tr[data-kind="p"] .cdd'); - if(pdd){pdd.click(); - const pHex=PALETTE.find(p=>p[0]!==MAP['p'])[0]; - const prow=[...document.querySelectorAll('.cddpop .cddrow')].find(r=>r.querySelector('.cddhx').textContent===pHex); - if(prow)prow.click(); - const pf=document.getElementById('uicr-fringe'),pfWant=contrast(pHex,'#aabbcc'); - A(prow&&pf&&Math.abs(parseFloat(pf.textContent)-pfWant)<0.06,'default-fg change re-rates a p-fallback face: got '+(pf&&pf.textContent.trim())+' want '+pfWant.toFixed(1)); - }else A(false,'syntax table has a p row with a dropdown'); - if(pLocked){LOCKED.add('p');buildTable();} - for(const k in MAP)delete MAP[k];Object.assign(MAP,saveMAP);for(const f in UIMAP)delete UIMAP[f];Object.assign(UIMAP,saveUI);buildUITable();applyGround(); - document.title='CONTRASTTEST '+(ok?'PASS':'FAIL'); - const d=document.createElement('div');d.id='contrasttest';d.textContent='CONTRASTTEST '+(ok?'PASS':'FAIL')+(notes.length?' | '+notes.join(' ; '):'');document.body.appendChild(d);} -// Bevel gate (open with #beveltest): released/pressed boxes derive their -// highlight and shadow from the face's effective bg per Emacs's relief -// algorithm, and pressed draws the shadow edge first. -if(location.hash==='#beveltest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}}; - const saveUI=JSON.parse(JSON.stringify(UIMAP)); - UIMAP['mode-line']={fg:'#d8dee9',bg:'#30343c',bold:false,italic:false,underline:false,strike:false,box:{style:'released',width:1,color:null}}; - buildUITable(); - const pv=document.getElementById('uiprev-mode-line'); - const bs=pv&&pv.style.boxShadow; - A(bs&&bs.includes('rgb(113, 118, 127)'),'released highlight derives from the face bg (#71767f): '+bs); - A(bs&&bs.includes('rgb(15, 17, 22)'),'released shadow derives from the face bg (#0f1116): '+bs); - UIMAP['mode-line'].box={style:'pressed',width:1,color:null};paintUI('mode-line'); - const bs2=pv&&pv.style.boxShadow; - A(bs2&&bs2.includes('rgb(15, 17, 22)')&&bs2.includes('rgb(113, 118, 127)')&&bs2.indexOf('rgb(15, 17, 22)')<bs2.indexOf('rgb(113, 118, 127)'),'pressed swaps the pair (shadow edge first): '+bs2); - UIMAP['mode-line'].box={style:'line',width:1,color:'#ff0000'};paintUI('mode-line'); - A(pv&&pv.style.boxShadow.includes('rgb(255, 0, 0)'),'line style keeps its explicit color: '+(pv&&pv.style.boxShadow)); - for(const f in UIMAP)delete UIMAP[f];Object.assign(UIMAP,saveUI);buildUITable(); - document.title='BEVELTEST '+(ok?'PASS':'FAIL'); - const d=document.createElement('div');d.id='beveltest';d.textContent='BEVELTEST '+(ok?'PASS':'FAIL')+(notes.length?' | '+notes.join(' ; '):'');document.body.appendChild(d);} -// Preview-link gate (open with #previewlinktest): known bespoke-preview face -// mappings stay wired to the face that Emacs actually uses. -if(location.hash==='#previewlinktest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}}; - const box=document.createElement('div'); - box.innerHTML=renderOrgPreview(); - const headline=[...box.querySelectorAll('[data-face="org-headline-todo"]')].find(e=>e.textContent.includes('Heading three')); - A(!!headline&&headline.previousElementSibling&&headline.previousElementSibling.dataset.face==='org-todo','org headline-todo follows a TODO keyword span'); - box.innerHTML=renderFlycheckPreview(); - const delim=[...box.querySelectorAll('[data-face="flycheck-error-delimiter"]')].map(e=>e.textContent).join(''); - const enclosed=[...box.querySelectorAll('[data-face="flycheck-delimited-error"]')].map(e=>e.textContent).join(''); - A(delim==='[]','flycheck delimiters use flycheck-error-delimiter'); - A(enclosed==='err','flycheck enclosed text uses flycheck-delimited-error'); - box.innerHTML=renderErcPreview(); - const own=[...box.querySelectorAll('[data-face="erc-input-face"]')].some(e=>e.textContent.includes('hello everyone')); - const bob=[...box.querySelectorAll('[data-face="erc-default-face"]')].some(e=>e.textContent.includes('hi craig')); - A(own,'erc own sent message uses erc-input-face'); - A(bob,'erc remote message uses erc-default-face'); - document.title='PREVIEWLINKTEST '+(ok?'PASS':'FAIL'); - const d=document.createElement('div');d.id='previewlinktest';d.textContent='PREVIEWLINKTEST '+(ok?'PASS':'FAIL')+(notes.length?' | '+notes.join(' ; '):'');document.body.appendChild(d);} -// Safe-lightness gate (open with #safetest): the OKLCH picker shades the unsafe -// lightness band for a selected covered face and hides it when no face is selected. -if(location.hash==='#safetest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}}; - const saveMAP=Object.assign({},MAP); - MAP['p']='#f0fef0';MAP['kw']='#67809c';MAP['bg']='#000000'; - document.getElementById('newhexstr').value='#202830';openPicker();setPkModel('oklch'); - setSafeFace('region'); - const band=document.getElementById('svsafe'); - A(band&&band.style.display==='block','safe band shows for an in-scope face'); - A(band&&parseFloat(band.style.height)>0,'safe band has a positive height: '+(band&&band.style.height)); - setSafeFace(''); - A(band&&band.style.display==='none','safe band hidden when no face is selected'); - for(const k in MAP)delete MAP[k];Object.assign(MAP,saveMAP); - setPkModel('hsv');closePicker(); - document.title='SAFETEST '+(ok?'PASS':'FAIL'); - const d=document.createElement('div');d.id='safetest';d.textContent='SAFETEST '+(ok?'PASS':'FAIL')+(notes.length?' | '+notes.join(' ; '):'');document.body.appendChild(d);} -// Gone-rebind gate (open with #healtest): deleting a named color then recreating -// the name re-points the assignments stranded on the old hex to the new color. -if(location.hash==='#healtest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}}; - const saveP=PALETTE.slice(),saveM=Object.assign({},MAP),saveU=JSON.parse(JSON.stringify(UIMAP)),savePK=JSON.parse(JSON.stringify(PKGMAP)),saveG=Object.assign({},lastGone),saveSel=selectedIdx; - PALETTE=[['#0d0b0a','ground'],['#cdced1','fg'],['#67809c','blue']];MAP['kw']='#67809c';lastGone={};selectedIdx=null;renderPalette();buildTable(); - const blue=[...document.querySelectorAll('#pals .pchip')].find(c=>c.querySelector('.nm')&&c.querySelector('.nm').value==='blue'); - A(!!(blue&&blue.querySelector('.rm')),'blue chip has a remove button'); - if(blue&&blue.querySelector('.rm'))blue.querySelector('.rm').click(); - A(!PALETTE.some(p=>p[1]==='blue'),'blue was deleted'); - A(lastGone['blue']==='#67809c','delete recorded the gone name->hex'); - document.getElementById('newhexstr').value='#5a7a9a';document.getElementById('newname').value='blue';selectedIdx=null;addColor(); - A(MAP['kw']==='#5a7a9a','assignment re-bound to the recreated name, got '+MAP['kw']); - A(!('blue' in lastGone),'heal consumed the gone entry'); - PALETTE=saveP;for(const k in MAP)delete MAP[k];Object.assign(MAP,saveM);for(const f in UIMAP)delete UIMAP[f];Object.assign(UIMAP,saveU);PKGMAP=savePK;lastGone=saveG;selectedIdx=saveSel; - renderPalette();buildTable();buildUITable();if(document.getElementById('pkgbody'))buildPkgTable(); - document.title='HEALTEST '+(ok?'PASS':'FAIL'); - const d=document.createElement('div');d.id='healtest';d.textContent='HEALTEST '+(ok?'PASS':'FAIL')+(notes.length?' | '+notes.join(' ; '):'');document.body.appendChild(d);} -// Column-strip gate (open with #columntest): the palette renders as a pinned -// ground column plus structural columns, chips keep their controls, and renaming -// a color leaves it in the same strip because the column id is stable. -if(location.hash==='#columntest'||location.hash==='#familytest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}}; - const saveP=PALETTE.slice(),saveM=Object.assign({},MAP),saveG=Object.assign({},lastGone),saveSel=selectedIdx; - MAP['bg']='#0d0b0a';MAP['p']='#f0fef0'; - PALETTE=[['#0d0b0a','ground'],['#f0fef0','fg'],['#c0402a','red'],['#3a6ea5','blue'],['#808080','gray']];selectedIdx=null;renderPalette(); - const strips=[...document.querySelectorAll('#pals .fstrip')]; - A(strips.length&&strips[0].dataset.column==='ground','ground column is pinned first'); - A(strips[0].querySelectorAll('.pchip').length===2,'ground column carries bg + fg endpoints'); - A(!!strips[0].querySelector('.fhead + .fcount + .pchip'),'span control sits between header and tiles for ground'); - A(strips.length>=4,'ground + red + blue + gray columns, got '+strips.length); - const blueHead=strips.find(s=>s.dataset.column==='blue')&&strips.find(s=>s.dataset.column==='blue').querySelector('.ctitle'); - A(!!blueHead,'normal column header has a selectable title'); - if(blueHead)blueHead.click(); - A(selectedIdx!==null&&PALETTE[selectedIdx][1]==='blue'&&document.getElementById('newhexstr').value.toLowerCase()==='#3a6ea5','clicking a column title selects its base color'); - const blueRight=strips.find(s=>s.dataset.column==='blue')&&strips.find(s=>s.dataset.column==='blue').querySelector('.cmove.right'); - if(blueRight)blueRight.click(); - const moved=[...document.querySelectorAll('#pals .fstrip')].map(s=>s.dataset.column); - A(moved.indexOf('blue')>moved.indexOf('gray'),'right arrow moves a color column after its neighbor'); - const redChip=[...document.querySelectorAll('#pals .pchip')].find(c=>c.querySelector('.nm')&&c.querySelector('.nm').value==='red'); - A(!!redChip&&!!redChip.querySelector('.rm')&&!!redChip.querySelector('.nm'),'a column chip keeps remove + rename controls'); - const redColumn=redChip&&redChip.closest('.fstrip').dataset.column; - const ri=PALETTE.findIndex(p=>p[1]==='red');PALETTE[ri][1]='zztop-absurd';renderPalette(); - const renamed=[...document.querySelectorAll('#pals .pchip')].find(c=>c.querySelector('.nm')&&c.querySelector('.nm').value==='zztop-absurd'); - A(!!renamed&&renamed.closest('.fstrip').dataset.column===redColumn,'a renamed color stays in the same strip'); - PALETTE=[['#0d0b0a','bg','ground'],['#f0fef0','fg','ground'],['#0d0b0a','bg2'],['#0d0b0a','bg-alt']];MAP['bg']='#0d0b0a';MAP['p']='#f0fef0';selectedIdx=null;renderPalette(); - const bg2Chip=[...document.querySelectorAll('#pals .pchip')].find(c=>c.querySelector('.nm')&&c.querySelector('.nm').value==='bg2'); - A(!!bg2Chip&&bg2Chip.closest('.fstrip').dataset.column==='bg2'&&!!bg2Chip.querySelector('.rm')&&!bg2Chip.querySelector('.lock'),'same-hex bg2 remains a normal removable color column chip'); - if(bg2Chip){bg2Chip.click();document.getElementById('newhexstr').value='#101820';document.getElementById('newname').value='bg2';updateColor();} - A(MAP['bg']==='#0d0b0a','editing same-hex bg2 does not repoint the real bg assignment'); - A(PALETTE.some(p=>p[1]==='bg2'&&p[0]==='#101820'),'editing same-hex bg2 updates only that palette tile'); - PALETTE=[['#0d0b0a','bg','ground'],['#f0fef0','fg','ground'],['#c0402a','red','red'],['#3a6ea5','blue','blue'],['#92acc2','blue+1','blue']]; - MAP['kw']='#3a6ea5';selectedIdx=2;clearPalette(); - A(PALETTE.length===2&&PALETTE.every(p=>groundRoleOfEntry(p,{bg:MAP['bg'],fg:MAP['p']})),'clear palette leaves only bg and fg tiles'); - A(!PALETTE.some(p=>p[1]==='red'||p[1]==='blue'||p[1]==='blue+1'),'clear palette removes normal color columns and spans'); - A(MAP['kw']==='#3a6ea5','clear palette leaves existing assignments on gone hexes'); - A(lastGone['blue']==='#3a6ea5'&&lastGone['blue+1']==='#92acc2','clear palette records removed names for recovery'); - A(selectedIdx===null,'clear palette clears selected color'); - PALETTE=saveP;for(const k in MAP)delete MAP[k];Object.assign(MAP,saveM);lastGone=saveG;selectedIdx=saveSel;renderPalette(); - document.title='COLUMNTEST '+(ok?'PASS':'FAIL'); - const d=document.createElement('div');d.id='columntest';d.textContent='COLUMNTEST '+(ok?'PASS':'FAIL')+(notes.length?' | '+notes.join(' ; '):'');document.body.appendChild(d);} -// Count-control gate (open with #counttest): the per-column count regenerates the -// column — count up adds symmetric steps, count down drops the extremes, a -// reference to a surviving step follows the new hex, a reference to a removed step -// is left on its old (now-gone) hex. -if(location.hash==='#counttest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}}; - const saveP=PALETTE.slice(),saveM=Object.assign({},MAP),saveU=JSON.parse(JSON.stringify(UIMAP)),saveSel=selectedIdx; - MAP['bg']='#204060';MAP['p']='#f0fef0'; - PALETTE=[['#204060','bg'],['#f0fef0','fg']]; - setGroundSpan(2); - A(MAP['bg']==='#204060'&&MAP['p']==='#f0fef0','spanning ground keeps bg/fg assignments on endpoints'); - A(PALETTE.some(p=>p[1]==='ground-1')&&PALETTE.some(p=>p[1]==='ground-2'),'spanning ground adds interior ground-N entries'); - A(document.querySelector('#pals .fstrip[data-column="ground"] .fhead + .fcount + .pchip'),'ground span control renders before tiles'); - MAP['bg']='#ffffff';MAP['p']='#000000'; - PALETTE=[['#ffffff','bg'],['#bbbbbb','ground-1','ground'],['#777777','ground-2','ground'],['#000000','fg']]; - renderPalette(); - const groundNames=[...document.querySelectorAll('#pals .fstrip[data-column="ground"] .pchip .nm')].map(e=>e.value); - A(groundNames.join('|')==='bg|ground-1|ground-2|fg','ground column order is bg, ground steps, fg even when bg is lighter: '+groundNames.join('|')); - MAP['bg']='#204060';MAP['p']='#f0fef0'; - setGroundSpan(1); - A(!PALETTE.some(p=>p[1]==='ground-2'),'lowering ground span removes dropped interior steps'); - PALETTE=[['#204060','bg'],['#f0fef0','fg']]; - regenColumn('#67809c',2).members.forEach(m=>PALETTE.push([m.hex,m.offset===0?'blue':'blue'+(m.offset>0?'+'+m.offset:m.offset)])); - const innerOld=regenColumn('#67809c',2).members.find(m=>m.offset===1).hex; // survives a count change - const outerOld=regenColumn('#67809c',2).members.find(m=>m.offset===2).hex; // dropped on count-down - UIMAP['region']={fg:null,bg:innerOld,bold:false,italic:false,underline:false,strike:false}; - UIMAP['highlight']={fg:null,bg:outerOld,bold:false,italic:false,underline:false,strike:false}; - selectedIdx=null;renderPalette(); - setColumnCount('#67809c',1); - const palHexes=new Set(PALETTE.map(p=>p[0].toLowerCase())); - A(!palHexes.has(outerOld.toLowerCase()),'outer step removed from palette on count down'); - A(UIMAP['highlight'].bg.toLowerCase()===outerOld.toLowerCase(),'a removed-step reference stays on its old (gone) hex'); - const newInner=regenColumn('#67809c',1).members.find(m=>m.offset===1).hex; - A(UIMAP['region'].bg.toLowerCase()===newInner.toLowerCase(),'a surviving-step reference followed the regenerate, got '+UIMAP['region'].bg); - setColumnCount('#67809c',3); - const want3=regenColumn('#67809c',3).members.map(m=>m.hex.toLowerCase()); - const have=new Set(PALETTE.map(p=>p[0].toLowerCase())); - A(want3.every(h=>have.has(h)),'count up to 3 adds all 7 ramp colors to the palette'); - PALETTE=saveP;for(const k in MAP)delete MAP[k];Object.assign(MAP,saveM);for(const f in UIMAP)delete UIMAP[f];Object.assign(UIMAP,saveU);selectedIdx=saveSel;renderPalette(); - document.title='COUNTTEST '+(ok?'PASS':'FAIL'); - const d=document.createElement('div');d.id='counttest';d.textContent='COUNTTEST '+(ok?'PASS':'FAIL')+(notes.length?' | '+notes.join(' ; '):'');document.body.appendChild(d);} -// Base-edit + ground-edit gate (open with #baseedittest): editing a column base -// recolors the whole column at the same count and references follow; editing a -// ground swatch writes the bg/fg assignment. -if(location.hash==='#baseedittest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}}; - const saveP=PALETTE.slice(),saveM=Object.assign({},MAP),saveU=JSON.parse(JSON.stringify(UIMAP)),saveSel=selectedIdx; - MAP['bg']='#0d0b0a';MAP['p']='#f0fef0'; - PALETTE=[['#0d0b0a','ground'],['#f0fef0','fg']]; - regenColumn('#67809c',2).members.forEach(m=>PALETTE.push([m.hex,m.offset===0?'blue':'blue'+(m.offset>0?'+'+m.offset:m.offset)])); - UIMAP['region']={fg:null,bg:'#67809c',bold:false,italic:false,underline:false,strike:false}; - renderPalette();buildUITable(); - selectedIdx=PALETTE.findIndex(p=>p[0].toLowerCase()==='#67809c'); - document.getElementById('newhexstr').value='#3a8a8a';document.getElementById('newname').value='teal'; - updateColor(); - const column=columnsFromPalette(PALETTE,{bg:MAP['bg'],fg:MAP['p']}).columns[0]; - A(column&&column.members.some(m=>m.hex.toLowerCase()==='#3a8a8a'),'column base recolored to the new hex'); - A(fam&&fam.members.length===5,'count preserved (±2 → 5 members), got '+(fam&&fam.members.length)); - A(!new Set(PALETTE.map(p=>p[0].toLowerCase())).has('#67809c'),'old base removed from palette'); - A(UIMAP['region'].bg.toLowerCase()==='#3a8a8a','a reference to the base followed to the new base hex'); - // ground edit: select bg, change hex, MAP.bg follows - selectedIdx=PALETTE.findIndex(p=>p[0].toLowerCase()==='#0d0b0a'); - document.getElementById('newhexstr').value='#101010';document.getElementById('newname').value='ground'; - updateColor(); - A(MAP['bg'].toLowerCase()==='#101010','editing the bg swatch wrote the bg assignment, got '+MAP['bg']); - PALETTE=saveP;for(const k in MAP)delete MAP[k];Object.assign(MAP,saveM);for(const f in UIMAP)delete UIMAP[f];Object.assign(UIMAP,saveU);selectedIdx=saveSel;renderPalette(); - document.title='BASEEDITTEST '+(ok?'PASS':'FAIL'); - const d=document.createElement('div');d.id='baseedittest';d.textContent='BASEEDITTEST '+(ok?'PASS':'FAIL')+(notes.length?' | '+notes.join(' ; '):'');document.body.appendChild(d);} -// Round-trip gate (open with #roundtriptest): export stays a flat palette with -// stable column ids, and import does not need color-derived column reconstruction. -if(location.hash==='#roundtriptest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}}; - const saveP=PALETTE.slice(),saveM=Object.assign({},MAP); - PALETTE=[['#ffffff','bg','ground'],['#000000','fg','ground'],['#224466','blue','blue'],['#446688','renamed-blue','blue']]; - MAP['bg']='#ffffff';MAP['p']='#000000'; - const before=JSON.stringify(exportObj()); - applyImported(before); - const after=JSON.stringify(exportObj()); - A(before===after,'export → import → export is byte-identical'); - const obj=JSON.parse(after); - A(Array.isArray(obj.palette)&&obj.palette.every(e=>Array.isArray(e)&&e.length>=3&&typeof e[2]==='string'),'exported palette carries flat [hex,name,columnId] entries'); - A(obj.palette.some(e=>e[1]==='renamed-blue'&&e[2]==='blue'),'renamed color keeps its stable column id through export/import'); - PALETTE=saveP;for(const k in MAP)delete MAP[k];Object.assign(MAP,saveM); - document.title='ROUNDTRIPTEST '+(ok?'PASS':'FAIL'); - const d=document.createElement('div');d.id='roundtriptest';d.textContent='ROUNDTRIPTEST '+(ok?'PASS':'FAIL')+(notes.length?' | '+notes.join(' ; '):'');document.body.appendChild(d);} +BROWSER_GATES_J |
