aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio/app.js
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-13 17:17:31 -0500
committerCraig Jennings <c@cjennings.net>2026-06-13 17:17:31 -0500
commitd93560446f954a44890b8472f90d57c3080993df (patch)
treeda5f94a93fb296709f1c1272a2c118ffee47dac5 /scripts/theme-studio/app.js
parentc18d914c138d04157afe64a64b7cd47aaa3171b0 (diff)
downloaddotemacs-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.js649
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}">&#128274;</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':''}>&#8249;</button><button class="ctitle" title="select base color"></button><button class="cmove right" title="move column right" ${position===count-1?'disabled':''}>&#8250;</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 &#177; <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