diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-15 22:07:45 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-15 22:07:45 -0500 |
| commit | de6fccc99c47f69da5c39718c1882c7342ee0e52 (patch) | |
| tree | dc80aae3de05e22068074e4ed02eebbbfb993577 | |
| parent | 48d8b2ccca1890aa33da8f49a6ddb661271a2c77 (diff) | |
| download | dotemacs-de6fccc99c47f69da5c39718c1882c7342ee0e52.tar.gz dotemacs-de6fccc99c47f69da5c39718c1882c7342ee0e52.zip | |
refactor(theme-studio): extract a groundPair() helper
The literal {bg:MAP['bg'],fg:MAP['p']} repeated 32 times across app.js, palette-actions.js, and the browser gates. Replace it with a groundPair() helper. Named groundPair, not ground, to avoid colliding with the local ground bindings destructured from columnsFromPalette. No behavior change; node tests and browser gates are the safety net.
| -rw-r--r-- | scripts/theme-studio/app.js | 18 | ||||
| -rw-r--r-- | scripts/theme-studio/browser-gates.js | 20 | ||||
| -rw-r--r-- | scripts/theme-studio/palette-actions.js | 34 | ||||
| -rw-r--r-- | scripts/theme-studio/theme-studio.html | 72 |
4 files changed, 76 insertions, 68 deletions
diff --git a/scripts/theme-studio/app.js b/scripts/theme-studio/app.js index cfa5b8705..a4e0da9c1 100644 --- a/scripts/theme-studio/app.js +++ b/scripts/theme-studio/app.js @@ -41,6 +41,10 @@ function crHtml(r,target=4.5){const v=verdictFor(r,target);return `<span style=" // tiers resolve their raw value through these before measuring or rendering. function effFg(v){return v||MAP['p'];} function effBg(v){return v||MAP['bg'];} +// The ground pair (background + default foreground), passed to every app-core +// helper that needs to resolve ground roles. Was the literal {bg:MAP['bg'], +// fg:MAP['p']} repeated across app.js, palette-actions.js, and the browser gates. +function groundPair(){return {bg:MAP['bg'],fg:MAP['p']};} function cid(l){return l.replace(/\W/g,'');} function buildLangSel(){const s=document.getElementById('langsel');s.innerHTML='';for(const lang in SAMPLES){const o=document.createElement('option');o.value=lang;o.textContent=lang;s.appendChild(o);}} function renderCode(){ @@ -68,11 +72,11 @@ function mkColorDropdown(options,cur,onPick,opts={}){ const nameOf=h=>{const o=options.find(p=>p[0]===h);return o?o[1]:(h||'none');}; const displayHex=h=>h||(opts.defaultHex||''); const displayName=h=>h?nameOf(h):(opts.defaultName||nameOf(h)); - function step(dir){if(wrap.dataset.locked==='1')return;const next=spanNeighborHex(cur,PALETTE,{bg:MAP['bg'],fg:MAP['p']},dir);if(!next)return;cur=next;paint();onPick(next);} + function step(dir){if(wrap.dataset.locked==='1')return;const next=spanNeighborHex(cur,PALETTE,groundPair(),dir);if(!next)return;cur=next;paint();onPick(next);} function paintStepButtons(){ const locked=wrap.dataset.locked==='1'; - left.disabled=locked||!spanNeighborHex(cur,PALETTE,{bg:MAP['bg'],fg:MAP['p']},-1); - right.disabled=locked||!spanNeighborHex(cur,PALETTE,{bg:MAP['bg'],fg:MAP['p']},1); + left.disabled=locked||!spanNeighborHex(cur,PALETTE,groundPair(),-1); + right.disabled=locked||!spanNeighborHex(cur,PALETTE,groundPair(),1); } function paint(){const shown=displayHex(cur),nm=displayName(cur),ttl=cur?(nm+' '+cur):(nm+(shown?' -> '+shown:''));t.style.background=shown||'#161412';t.style.color=shown?textOn(shown):'#b4b1a2';t.dataset.val=cur||'';t.title=ttl;t.classList.toggle('is-default',!cur);t.classList.toggle('gone',!!cur&&nameOf(cur)==='(gone)'); t.innerHTML=opts.compact?`<span class="cddsw" style="background:${shown||'transparent'}"></span>`:`<span class="cddsw" style="background:${shown||'transparent'}"></span>${esc(nm)}`;paintStepButtons();} @@ -84,7 +88,7 @@ function mkColorDropdown(options,cur,onPick,opts={}){ // then one row per family) instead of a long vertical list. galleryModel is // the shared pure layout (app-core.js). const pop=document.createElement('div');pop.className='cddpop cddgrid'; - const model=galleryModel(cur,PALETTE,{bg:MAP['bg'],fg:MAP['p']}); + const model=galleryModel(cur,PALETTE,groundPair()); const pick=(hex)=>{cur=hex;paint();closeColorDropdown();onPick(hex);}; const head=document.createElement('div');head.className='cddghead'; const def=document.createElement('button');def.type='button'; @@ -118,7 +122,7 @@ function mkColorDropdown(options,cur,onPick,opts={}){ // palette in the same ground/column order as the palette panel. If cur is set // but no longer in the palette, surface it as a "(gone)" entry so the row still // shows what it points at. Shared by all three tiers. -function ddList(cur){return paletteOptionList(cur,PALETTE,{bg:MAP['bg'],fg:MAP['p']});} +function ddList(cur){return paletteOptionList(cur,PALETTE,groundPair());} // Shared lock toggle for any table row. lockKey is namespaced per tier (bare // syntax kind, 'ui:'+face, 'pkg:'+app+':'+face). els are the row's editable // controls — native selects/buttons/inputs are disabled; the custom swatch @@ -228,13 +232,13 @@ function applyEdit(){if(selectedIdx!==null)updateColor();else addColor();} function selectColor(i){selectedIdx=i;GEN_SELECTION=null;const [hex,name]=PALETTE[i];setHex(hex);document.getElementById('newname').value=name;renderPalette();renderGeneratorPreview();notify('editing "'+name+'" — change the value, then Enter (or Update selected) to save',false);} function updateColor(){ if(selectedIdx===null){notify('click a palette color to select it first',true);return;} - const i=selectedIdx,oldHex=PALETTE[i][0],oldRole=groundRoleOfEntry(PALETTE[i],{bg:MAP['bg'],fg:MAP['p']}); + const i=selectedIdx,oldHex=PALETTE[i][0],oldRole=groundRoleOfEntry(PALETTE[i],groundPair()); const newHex=curHex(); const newName=(document.getElementById('newname').value.trim())||PALETTE[i][1]; if(PALETTE.some((p,j)=>j!==i&&p[1].toLowerCase()===newName.toLowerCase())){notify('another color is already named "'+newName+'" — names must be unique',true);return;} const isGroundEdit=oldRole==='bg'||oldRole==='fg'; // If the edited color is a column base with a ramp, recolor the whole column: regenerate from the new base at the same count. - const columns=columnsFromPalette(PALETTE,{bg:MAP['bg'],fg:MAP['p']}).columns; + const columns=columnsFromPalette(PALETTE,groundPair()).columns; const column=isGroundEdit?null:columns.find(f=>f.base.toLowerCase()===oldHex.toLowerCase()); const count=column?Math.max(0,...rankByLightness(column.members.map(m=>m.hex),column.base).map(m=>Math.abs(m.offset))):0; const columnId=isGroundEdit?'ground':(PALETTE[i][2]||columnStem(PALETTE[i][1])); diff --git a/scripts/theme-studio/browser-gates.js b/scripts/theme-studio/browser-gates.js index 18f0b80c6..478865785 100644 --- a/scripts/theme-studio/browser-gates.js +++ b/scripts/theme-studio/browser-gates.js @@ -557,7 +557,7 @@ if(location.hash==='#columntest'){let ok=true;const notes=[];const A=(c,n)=>{if( window.confirm=oldConfirm; A(!PALETTE.some(p=>p[2]==='blue'),'column delete removes every entry with the stable column id'); A(PALETTE.some(p=>p[1]==='red')&&PALETTE.some(p=>p[1]==='gray'),'column delete leaves neighboring columns alone'); - A(PALETTE.some(p=>groundRoleOfEntry(p,{bg:MAP['bg'],fg:MAP['p']})==='bg')&&PALETTE.some(p=>groundRoleOfEntry(p,{bg:MAP['bg'],fg:MAP['p']})==='fg'),'column delete leaves ground entries alone'); + A(PALETTE.some(p=>groundRoleOfEntry(p,groundPair())==='bg')&&PALETTE.some(p=>groundRoleOfEntry(p,groundPair())==='fg'),'column delete leaves ground entries alone'); A(MAP['kw']==='#92acc2','column delete leaves face references on removed hexes'); buildTable(); const goneTitle=document.querySelector('#legbody tr[data-kind="kw"] .cdd')?.title||''; @@ -566,7 +566,7 @@ if(location.hash==='#columntest'){let ok=true;const notes=[];const A=(c,n)=>{if( A(selectedIdx===null,'column delete clears selected color'); PALETTE=[['#0d0b0a','bg','ground'],['#f0fef0','fg','ground'],['#c0402a','red','red'],['#3a6ea5','blue','blue'],['#92acc2','blue+1','blue']]; setSyntaxFg('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.length===2&&PALETTE.every(p=>groundRoleOfEntry(p,groundPair())),'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 face references on gone hexes'); A(lastGone['blue']==='#3a6ea5'&&lastGone['blue+1']==='#92acc2','clear palette records removed names for recovery'); @@ -601,9 +601,9 @@ if(location.hash==='#counttest'){let ok=true;const notes=[];const A=(c,n)=>{if(! setColumnCount('#101010',4); A(!PALETTE.some(p=>p[0].toLowerCase()==='#000000'&&p[1]!=='bg'),'spanning a near-black base skips generated pure-black tiles'); PALETTE=[['#204060','bg'],['#f0fef0','fg']]; - regenColumn('#67809c',2,{ground:{bg:MAP['bg'],fg:MAP['p']}}).members.forEach(m=>PALETTE.push([m.hex,m.offset===0?'blue':'blue'+(m.offset>0?'+'+m.offset:m.offset)])); - const innerOld=regenColumn('#67809c',2,{ground:{bg:MAP['bg'],fg:MAP['p']}}).members.find(m=>m.offset===1).hex; // survives a count change - const outerOld=regenColumn('#67809c',2,{ground:{bg:MAP['bg'],fg:MAP['p']}}).members.find(m=>m.offset===2).hex; // dropped on count-down + regenColumn('#67809c',2,{ground:groundPair()}).members.forEach(m=>PALETTE.push([m.hex,m.offset===0?'blue':'blue'+(m.offset>0?'+'+m.offset:m.offset)])); + const innerOld=regenColumn('#67809c',2,{ground:groundPair()}).members.find(m=>m.offset===1).hex; // survives a count change + const outerOld=regenColumn('#67809c',2,{ground:groundPair()}).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(); @@ -613,10 +613,10 @@ if(location.hash==='#counttest'){let ok=true;const notes=[];const A=(c,n)=>{if(! 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,{ground:{bg:MAP['bg'],fg:MAP['p']}}).members.find(m=>m.offset===1).hex; + const newInner=regenColumn('#67809c',1,{ground:groundPair()}).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,{ground:{bg:MAP['bg'],fg:MAP['p']}}).members.map(m=>m.hex.toLowerCase()); + const want3=regenColumn('#67809c',3,{ground:groundPair()}).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 span colors to the palette'); {const _lum=h=>{const n=parseInt(h.slice(1),16),r=(n>>16&255)/255,g=(n>>8&255)/255,b=(n&255)/255;const f=c=>c<=0.03928?c/12.92:((c+0.055)/1.055)**2.4;return 0.2126*f(r)+0.7152*f(g)+0.0722*f(b);}; @@ -632,13 +632,13 @@ if(location.hash==='#baseedittest'){let ok=true;const notes=[];const A=(c,n)=>{i const saveP=PALETTE.slice(),saveM=Object.assign({},MAP),saveU=JSON.parse(JSON.stringify(UIMAP)),saveSel=selectedIdx; setSyntaxFg('bg','#0d0b0a');setSyntaxFg('p','#f0fef0'); PALETTE=[['#0d0b0a','ground'],['#f0fef0','fg']]; - regenColumn('#67809c',2,{ground:{bg:MAP['bg'],fg:MAP['p']}}).members.forEach(m=>PALETTE.push([m.hex,m.offset===0?'blue':'blue'+(m.offset>0?'+'+m.offset:m.offset)])); + regenColumn('#67809c',2,{ground:groundPair()}).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]; + const column=columnsFromPalette(PALETTE,groundPair()).columns[0]; A(column&&column.members.some(m=>m.hex.toLowerCase()==='#3a8a8a'),'column base recolored to the new hex'); A(column&&column.members.length===5,'count preserved (±2 → 5 members), got '+(column&&column.members.length)); A(!new Set(PALETTE.map(p=>p[0].toLowerCase())).has('#67809c'),'old base removed from palette'); @@ -746,7 +746,7 @@ if(location.hash==='#paltoggletest'){let ok=true;const notes=[];const A=(c,n)=>{ const saveP=PALETTE.slice(),saveM=Object.assign({},MAP); setSyntaxFg('bg','#101010');setSyntaxFg('p','#f0f0f0'); PALETTE=[['#101010','bg','ground'],['#f0f0f0','fg','ground']]; - regenColumn('#67809c',2,{ground:{bg:MAP['bg'],fg:MAP['p']}}).members.forEach(m=>PALETTE.push([m.hex,m.offset===0?'blue':'blue'+(m.offset>0?'+'+m.offset:m.offset),'blue'])); + regenColumn('#67809c',2,{ground:groundPair()}).members.forEach(m=>PALETTE.push([m.hex,m.offset===0?'blue':'blue'+(m.offset>0?'+'+m.offset:m.offset),'blue'])); renderPalette(); const tg=document.getElementById('paltoggle'); A(!!tg,'palette-toggle-present'); diff --git a/scripts/theme-studio/palette-actions.js b/scripts/theme-studio/palette-actions.js index 0f49225a8..21e4f75f0 100644 --- a/scripts/theme-studio/palette-actions.js +++ b/scripts/theme-studio/palette-actions.js @@ -1,6 +1,6 @@ function clearPalette(){ normalizePalette(); - const plan=clearPalettePlan(PALETTE,{bg:MAP['bg'],fg:MAP['p']}); + const plan=clearPalettePlan(PALETTE,groundPair()); plan.removed.forEach(({hex,name})=>rememberGone(hex,name)); PALETTE=plan.palette;selectedIdx=null; refreshPaletteState(); @@ -34,17 +34,17 @@ function normalizePaletteEntry(entry){ return [hex,name,(entry&&entry[2])||columnIdOf(entry)]; } function ensureGroundEndpoints(){ - const ground={bg:MAP['bg'],fg:MAP['p']}; - if(ground.bg&&!PALETTE.some(entry=>groundRoleOfEntry(entry,ground)==='bg'))PALETTE.unshift([ground.bg,'bg','ground']); - if(ground.fg&&!PALETTE.some(entry=>groundRoleOfEntry(entry,ground)==='fg'))PALETTE.push([ground.fg,'fg','ground']); + const g=groundPair(); + if(g.bg&&!PALETTE.some(entry=>groundRoleOfEntry(entry,g)==='bg'))PALETTE.unshift([g.bg,'bg','ground']); + if(g.fg&&!PALETTE.some(entry=>groundRoleOfEntry(entry,g)==='fg'))PALETTE.push([g.fg,'fg','ground']); } function normalizePalette(){PALETTE=PALETTE.map(normalizePaletteEntry);ensureGroundEndpoints();} // 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(){ - return groundColumnMembersFromPalette(PALETTE,{bg:MAP['bg'],fg:MAP['p']}); + return groundColumnMembersFromPalette(PALETTE,groundPair()); } -function groundSpanCount(){return PALETTE.filter(entry=>groundRoleOfEntry(entry,{bg:MAP['bg'],fg:MAP['p']})==='step').length;} +function groundSpanCount(){return PALETTE.filter(entry=>groundRoleOfEntry(entry,groundPair())==='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>`; @@ -52,7 +52,7 @@ function groundSpanControl(){ return d; } function setGroundSpan(n){ - const old=PALETTE.filter(entry=>groundRoleOfEntry(entry,{bg:MAP['bg'],fg:MAP['p']})==='step'); + const old=PALETTE.filter(entry=>groundRoleOfEntry(entry,groundPair())==='step'); const bg=srgb2oklab(MAP['bg']),fg=srgb2oklab(MAP['p']); const entries=[]; let step=1; @@ -67,8 +67,8 @@ function setGroundSpan(n){ 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'); + for(let i=PALETTE.length-1;i>=0;i--)if(groundRoleOfEntry(PALETTE[i],groundPair())==='step')PALETTE.splice(i,1); + let at=PALETTE.findIndex(entry=>groundRoleOfEntry(entry,groundPair())==='bg'); if(at<0)at=0; else at+=1; PALETTE.splice(Math.min(at,PALETTE.length),0,...entries); selectedIdx=null;refreshPaletteState(); @@ -89,7 +89,7 @@ function renderPaletteWarnings(warnings,overflow){ // Families sort deterministically, so the old move-arrow / drag reordering is gone. function paletteChip(i,nearest,used,scopes){ const [hex,name]=PALETTE[i],tc=textOn(hex),nde=nearest[i]; - const role=groundRoleOfEntry(PALETTE[i],{bg:MAP['bg'],fg:MAP['p']}); + const role=groundRoleOfEntry(PALETTE[i],groundPair()); const locked=(role==='bg'||role==='fg'); const d=document.createElement('div');d.className='pchip'+(i===selectedIdx?' sel':'');d.style.background=hex; d.dataset.paletteIndex=String(i); @@ -126,11 +126,11 @@ function selectColumnBase(f){ if(i>=0)selectColor(i); } function isGroundEntry(entry){ - return !!groundRoleOfEntry(entry,{bg:MAP['bg'],fg:MAP['p']}); + return !!groundRoleOfEntry(entry,groundPair()); } function moveColumn(columnId,dir){ normalizePalette(); - const columns=sortColumns(columnsFromPalette(PALETTE,{bg:MAP['bg'],fg:MAP['p']}).columns); + const columns=sortColumns(columnsFromPalette(PALETTE,groundPair()).columns); const pos=columns.findIndex(f=>f.column===columnId); const next=columns[pos+dir]; if(pos<0||!next)return; @@ -149,7 +149,7 @@ function moveColumn(columnId,dir){ } function deleteColumn(columnId,label){ normalizePalette(); - const plan=deletePaletteColumnPlan(PALETTE,{bg:MAP['bg'],fg:MAP['p']},columnId); + const plan=deletePaletteColumnPlan(PALETTE,groundPair(),columnId); if(!plan.removed.length){notify('nothing to delete in "'+(label||columnId)+'"',true);return;} const title=label||columnId; if(!confirm('Delete color column "'+title+'"?\n\nThis removes '+plan.removed.length+' palette color(s). Existing face assignments will stay on their old hex values and show as "(gone)".'))return; @@ -184,8 +184,8 @@ function renderPalette(){ tg.onclick=()=>{paletteShowFull=!paletteShowFull;renderPalette();}; p.appendChild(tg); const {warnings,overflow,nearest}=paletteWarnings(PALETTE,DELTAE_MIN,5); - const {ground,columns}=columnsFromPalette(PALETTE,{bg:MAP['bg'],fg:MAP['p']}); - const usedHexes=usedPaletteHexes(PALETTE,SYNTAX,UIMAP,PKGMAP,{bg:MAP['bg'],fg:MAP['p']}); + const {ground,columns}=columnsFromPalette(PALETTE,groundPair()); + const usedHexes=usedPaletteHexes(PALETTE,SYNTAX,UIMAP,PKGMAP,groundPair()); // Per-view-area scopes for the hover "view area > element" usage list. Area // names match the view dropdown; elements use each tier's display label. const usageScopes=[ @@ -237,7 +237,7 @@ function columnCountControl(f){ // 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,{ground:{bg:MAP['bg'],fg:MAP['p']}}); + const r=regenColumn(baseHex,n,{ground:groundPair()}); 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())); @@ -251,7 +251,7 @@ function regenColumnInPlace(oldHexes,baseHex,baseName,n,columnId){ return plan.removed.length; } function setColumnCount(baseHex,n){ - const {columns}=columnsFromPalette(PALETTE,{bg:MAP['bg'],fg:MAP['p']}); + const {columns}=columnsFromPalette(PALETTE,groundPair()); 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'; diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html index 9b71fc9be..7fae1f674 100644 --- a/scripts/theme-studio/theme-studio.html +++ b/scripts/theme-studio/theme-studio.html @@ -1358,6 +1358,10 @@ function crHtml(r,target=4.5){const v=verdictFor(r,target);return `<span style=" // tiers resolve their raw value through these before measuring or rendering. function effFg(v){return v||MAP['p'];} function effBg(v){return v||MAP['bg'];} +// The ground pair (background + default foreground), passed to every app-core +// helper that needs to resolve ground roles. Was the literal {bg:MAP['bg'], +// fg:MAP['p']} repeated across app.js, palette-actions.js, and the browser gates. +function groundPair(){return {bg:MAP['bg'],fg:MAP['p']};} function cid(l){return l.replace(/\W/g,'');} function buildLangSel(){const s=document.getElementById('langsel');s.innerHTML='';for(const lang in SAMPLES){const o=document.createElement('option');o.value=lang;o.textContent=lang;s.appendChild(o);}} function renderCode(){ @@ -1385,11 +1389,11 @@ function mkColorDropdown(options,cur,onPick,opts={}){ const nameOf=h=>{const o=options.find(p=>p[0]===h);return o?o[1]:(h||'none');}; const displayHex=h=>h||(opts.defaultHex||''); const displayName=h=>h?nameOf(h):(opts.defaultName||nameOf(h)); - function step(dir){if(wrap.dataset.locked==='1')return;const next=spanNeighborHex(cur,PALETTE,{bg:MAP['bg'],fg:MAP['p']},dir);if(!next)return;cur=next;paint();onPick(next);} + function step(dir){if(wrap.dataset.locked==='1')return;const next=spanNeighborHex(cur,PALETTE,groundPair(),dir);if(!next)return;cur=next;paint();onPick(next);} function paintStepButtons(){ const locked=wrap.dataset.locked==='1'; - left.disabled=locked||!spanNeighborHex(cur,PALETTE,{bg:MAP['bg'],fg:MAP['p']},-1); - right.disabled=locked||!spanNeighborHex(cur,PALETTE,{bg:MAP['bg'],fg:MAP['p']},1); + left.disabled=locked||!spanNeighborHex(cur,PALETTE,groundPair(),-1); + right.disabled=locked||!spanNeighborHex(cur,PALETTE,groundPair(),1); } function paint(){const shown=displayHex(cur),nm=displayName(cur),ttl=cur?(nm+' '+cur):(nm+(shown?' -> '+shown:''));t.style.background=shown||'#161412';t.style.color=shown?textOn(shown):'#b4b1a2';t.dataset.val=cur||'';t.title=ttl;t.classList.toggle('is-default',!cur);t.classList.toggle('gone',!!cur&&nameOf(cur)==='(gone)'); t.innerHTML=opts.compact?`<span class="cddsw" style="background:${shown||'transparent'}"></span>`:`<span class="cddsw" style="background:${shown||'transparent'}"></span>${esc(nm)}`;paintStepButtons();} @@ -1401,7 +1405,7 @@ function mkColorDropdown(options,cur,onPick,opts={}){ // then one row per family) instead of a long vertical list. galleryModel is // the shared pure layout (app-core.js). const pop=document.createElement('div');pop.className='cddpop cddgrid'; - const model=galleryModel(cur,PALETTE,{bg:MAP['bg'],fg:MAP['p']}); + const model=galleryModel(cur,PALETTE,groundPair()); const pick=(hex)=>{cur=hex;paint();closeColorDropdown();onPick(hex);}; const head=document.createElement('div');head.className='cddghead'; const def=document.createElement('button');def.type='button'; @@ -1435,7 +1439,7 @@ function mkColorDropdown(options,cur,onPick,opts={}){ // palette in the same ground/column order as the palette panel. If cur is set // but no longer in the palette, surface it as a "(gone)" entry so the row still // shows what it points at. Shared by all three tiers. -function ddList(cur){return paletteOptionList(cur,PALETTE,{bg:MAP['bg'],fg:MAP['p']});} +function ddList(cur){return paletteOptionList(cur,PALETTE,groundPair());} // Shared lock toggle for any table row. lockKey is namespaced per tier (bare // syntax kind, 'ui:'+face, 'pkg:'+app+':'+face). els are the row's editable // controls — native selects/buttons/inputs are disabled; the custom swatch @@ -1541,7 +1545,7 @@ function buildTable(){ } function clearPalette(){ normalizePalette(); - const plan=clearPalettePlan(PALETTE,{bg:MAP['bg'],fg:MAP['p']}); + const plan=clearPalettePlan(PALETTE,groundPair()); plan.removed.forEach(({hex,name})=>rememberGone(hex,name)); PALETTE=plan.palette;selectedIdx=null; refreshPaletteState(); @@ -1575,17 +1579,17 @@ function normalizePaletteEntry(entry){ return [hex,name,(entry&&entry[2])||columnIdOf(entry)]; } function ensureGroundEndpoints(){ - const ground={bg:MAP['bg'],fg:MAP['p']}; - if(ground.bg&&!PALETTE.some(entry=>groundRoleOfEntry(entry,ground)==='bg'))PALETTE.unshift([ground.bg,'bg','ground']); - if(ground.fg&&!PALETTE.some(entry=>groundRoleOfEntry(entry,ground)==='fg'))PALETTE.push([ground.fg,'fg','ground']); + const g=groundPair(); + if(g.bg&&!PALETTE.some(entry=>groundRoleOfEntry(entry,g)==='bg'))PALETTE.unshift([g.bg,'bg','ground']); + if(g.fg&&!PALETTE.some(entry=>groundRoleOfEntry(entry,g)==='fg'))PALETTE.push([g.fg,'fg','ground']); } function normalizePalette(){PALETTE=PALETTE.map(normalizePaletteEntry);ensureGroundEndpoints();} // 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(){ - return groundColumnMembersFromPalette(PALETTE,{bg:MAP['bg'],fg:MAP['p']}); + return groundColumnMembersFromPalette(PALETTE,groundPair()); } -function groundSpanCount(){return PALETTE.filter(entry=>groundRoleOfEntry(entry,{bg:MAP['bg'],fg:MAP['p']})==='step').length;} +function groundSpanCount(){return PALETTE.filter(entry=>groundRoleOfEntry(entry,groundPair())==='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>`; @@ -1593,7 +1597,7 @@ function groundSpanControl(){ return d; } function setGroundSpan(n){ - const old=PALETTE.filter(entry=>groundRoleOfEntry(entry,{bg:MAP['bg'],fg:MAP['p']})==='step'); + const old=PALETTE.filter(entry=>groundRoleOfEntry(entry,groundPair())==='step'); const bg=srgb2oklab(MAP['bg']),fg=srgb2oklab(MAP['p']); const entries=[]; let step=1; @@ -1608,8 +1612,8 @@ function setGroundSpan(n){ 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'); + for(let i=PALETTE.length-1;i>=0;i--)if(groundRoleOfEntry(PALETTE[i],groundPair())==='step')PALETTE.splice(i,1); + let at=PALETTE.findIndex(entry=>groundRoleOfEntry(entry,groundPair())==='bg'); if(at<0)at=0; else at+=1; PALETTE.splice(Math.min(at,PALETTE.length),0,...entries); selectedIdx=null;refreshPaletteState(); @@ -1630,7 +1634,7 @@ function renderPaletteWarnings(warnings,overflow){ // Families sort deterministically, so the old move-arrow / drag reordering is gone. function paletteChip(i,nearest,used,scopes){ const [hex,name]=PALETTE[i],tc=textOn(hex),nde=nearest[i]; - const role=groundRoleOfEntry(PALETTE[i],{bg:MAP['bg'],fg:MAP['p']}); + const role=groundRoleOfEntry(PALETTE[i],groundPair()); const locked=(role==='bg'||role==='fg'); const d=document.createElement('div');d.className='pchip'+(i===selectedIdx?' sel':'');d.style.background=hex; d.dataset.paletteIndex=String(i); @@ -1667,11 +1671,11 @@ function selectColumnBase(f){ if(i>=0)selectColor(i); } function isGroundEntry(entry){ - return !!groundRoleOfEntry(entry,{bg:MAP['bg'],fg:MAP['p']}); + return !!groundRoleOfEntry(entry,groundPair()); } function moveColumn(columnId,dir){ normalizePalette(); - const columns=sortColumns(columnsFromPalette(PALETTE,{bg:MAP['bg'],fg:MAP['p']}).columns); + const columns=sortColumns(columnsFromPalette(PALETTE,groundPair()).columns); const pos=columns.findIndex(f=>f.column===columnId); const next=columns[pos+dir]; if(pos<0||!next)return; @@ -1690,7 +1694,7 @@ function moveColumn(columnId,dir){ } function deleteColumn(columnId,label){ normalizePalette(); - const plan=deletePaletteColumnPlan(PALETTE,{bg:MAP['bg'],fg:MAP['p']},columnId); + const plan=deletePaletteColumnPlan(PALETTE,groundPair(),columnId); if(!plan.removed.length){notify('nothing to delete in "'+(label||columnId)+'"',true);return;} const title=label||columnId; if(!confirm('Delete color column "'+title+'"?\n\nThis removes '+plan.removed.length+' palette color(s). Existing face assignments will stay on their old hex values and show as "(gone)".'))return; @@ -1725,8 +1729,8 @@ function renderPalette(){ tg.onclick=()=>{paletteShowFull=!paletteShowFull;renderPalette();}; p.appendChild(tg); const {warnings,overflow,nearest}=paletteWarnings(PALETTE,DELTAE_MIN,5); - const {ground,columns}=columnsFromPalette(PALETTE,{bg:MAP['bg'],fg:MAP['p']}); - const usedHexes=usedPaletteHexes(PALETTE,SYNTAX,UIMAP,PKGMAP,{bg:MAP['bg'],fg:MAP['p']}); + const {ground,columns}=columnsFromPalette(PALETTE,groundPair()); + const usedHexes=usedPaletteHexes(PALETTE,SYNTAX,UIMAP,PKGMAP,groundPair()); // Per-view-area scopes for the hover "view area > element" usage list. Area // names match the view dropdown; elements use each tier's display label. const usageScopes=[ @@ -1778,7 +1782,7 @@ function columnCountControl(f){ // 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,{ground:{bg:MAP['bg'],fg:MAP['p']}}); + const r=regenColumn(baseHex,n,{ground:groundPair()}); 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())); @@ -1792,7 +1796,7 @@ function regenColumnInPlace(oldHexes,baseHex,baseName,n,columnId){ return plan.removed.length; } function setColumnCount(baseHex,n){ - const {columns}=columnsFromPalette(PALETTE,{bg:MAP['bg'],fg:MAP['p']}); + const {columns}=columnsFromPalette(PALETTE,groundPair()); 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'; @@ -1806,13 +1810,13 @@ function applyEdit(){if(selectedIdx!==null)updateColor();else addColor();} function selectColor(i){selectedIdx=i;GEN_SELECTION=null;const [hex,name]=PALETTE[i];setHex(hex);document.getElementById('newname').value=name;renderPalette();renderGeneratorPreview();notify('editing "'+name+'" — change the value, then Enter (or Update selected) to save',false);} function updateColor(){ if(selectedIdx===null){notify('click a palette color to select it first',true);return;} - const i=selectedIdx,oldHex=PALETTE[i][0],oldRole=groundRoleOfEntry(PALETTE[i],{bg:MAP['bg'],fg:MAP['p']}); + const i=selectedIdx,oldHex=PALETTE[i][0],oldRole=groundRoleOfEntry(PALETTE[i],groundPair()); const newHex=curHex(); const newName=(document.getElementById('newname').value.trim())||PALETTE[i][1]; if(PALETTE.some((p,j)=>j!==i&&p[1].toLowerCase()===newName.toLowerCase())){notify('another color is already named "'+newName+'" — names must be unique',true);return;} const isGroundEdit=oldRole==='bg'||oldRole==='fg'; // If the edited color is a column base with a ramp, recolor the whole column: regenerate from the new base at the same count. - const columns=columnsFromPalette(PALETTE,{bg:MAP['bg'],fg:MAP['p']}).columns; + const columns=columnsFromPalette(PALETTE,groundPair()).columns; const column=isGroundEdit?null:columns.find(f=>f.base.toLowerCase()===oldHex.toLowerCase()); const count=column?Math.max(0,...rankByLightness(column.members.map(m=>m.hex),column.base).map(m=>Math.abs(m.offset))):0; const columnId=isGroundEdit?'ground':(PALETTE[i][2]||columnStem(PALETTE[i][1])); @@ -3176,7 +3180,7 @@ if(location.hash==='#columntest'){let ok=true;const notes=[];const A=(c,n)=>{if( window.confirm=oldConfirm; A(!PALETTE.some(p=>p[2]==='blue'),'column delete removes every entry with the stable column id'); A(PALETTE.some(p=>p[1]==='red')&&PALETTE.some(p=>p[1]==='gray'),'column delete leaves neighboring columns alone'); - A(PALETTE.some(p=>groundRoleOfEntry(p,{bg:MAP['bg'],fg:MAP['p']})==='bg')&&PALETTE.some(p=>groundRoleOfEntry(p,{bg:MAP['bg'],fg:MAP['p']})==='fg'),'column delete leaves ground entries alone'); + A(PALETTE.some(p=>groundRoleOfEntry(p,groundPair())==='bg')&&PALETTE.some(p=>groundRoleOfEntry(p,groundPair())==='fg'),'column delete leaves ground entries alone'); A(MAP['kw']==='#92acc2','column delete leaves face references on removed hexes'); buildTable(); const goneTitle=document.querySelector('#legbody tr[data-kind="kw"] .cdd')?.title||''; @@ -3185,7 +3189,7 @@ if(location.hash==='#columntest'){let ok=true;const notes=[];const A=(c,n)=>{if( A(selectedIdx===null,'column delete clears selected color'); PALETTE=[['#0d0b0a','bg','ground'],['#f0fef0','fg','ground'],['#c0402a','red','red'],['#3a6ea5','blue','blue'],['#92acc2','blue+1','blue']]; setSyntaxFg('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.length===2&&PALETTE.every(p=>groundRoleOfEntry(p,groundPair())),'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 face references on gone hexes'); A(lastGone['blue']==='#3a6ea5'&&lastGone['blue+1']==='#92acc2','clear palette records removed names for recovery'); @@ -3220,9 +3224,9 @@ if(location.hash==='#counttest'){let ok=true;const notes=[];const A=(c,n)=>{if(! setColumnCount('#101010',4); A(!PALETTE.some(p=>p[0].toLowerCase()==='#000000'&&p[1]!=='bg'),'spanning a near-black base skips generated pure-black tiles'); PALETTE=[['#204060','bg'],['#f0fef0','fg']]; - regenColumn('#67809c',2,{ground:{bg:MAP['bg'],fg:MAP['p']}}).members.forEach(m=>PALETTE.push([m.hex,m.offset===0?'blue':'blue'+(m.offset>0?'+'+m.offset:m.offset)])); - const innerOld=regenColumn('#67809c',2,{ground:{bg:MAP['bg'],fg:MAP['p']}}).members.find(m=>m.offset===1).hex; // survives a count change - const outerOld=regenColumn('#67809c',2,{ground:{bg:MAP['bg'],fg:MAP['p']}}).members.find(m=>m.offset===2).hex; // dropped on count-down + regenColumn('#67809c',2,{ground:groundPair()}).members.forEach(m=>PALETTE.push([m.hex,m.offset===0?'blue':'blue'+(m.offset>0?'+'+m.offset:m.offset)])); + const innerOld=regenColumn('#67809c',2,{ground:groundPair()}).members.find(m=>m.offset===1).hex; // survives a count change + const outerOld=regenColumn('#67809c',2,{ground:groundPair()}).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(); @@ -3232,10 +3236,10 @@ if(location.hash==='#counttest'){let ok=true;const notes=[];const A=(c,n)=>{if(! 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,{ground:{bg:MAP['bg'],fg:MAP['p']}}).members.find(m=>m.offset===1).hex; + const newInner=regenColumn('#67809c',1,{ground:groundPair()}).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,{ground:{bg:MAP['bg'],fg:MAP['p']}}).members.map(m=>m.hex.toLowerCase()); + const want3=regenColumn('#67809c',3,{ground:groundPair()}).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 span colors to the palette'); {const _lum=h=>{const n=parseInt(h.slice(1),16),r=(n>>16&255)/255,g=(n>>8&255)/255,b=(n&255)/255;const f=c=>c<=0.03928?c/12.92:((c+0.055)/1.055)**2.4;return 0.2126*f(r)+0.7152*f(g)+0.0722*f(b);}; @@ -3251,13 +3255,13 @@ if(location.hash==='#baseedittest'){let ok=true;const notes=[];const A=(c,n)=>{i const saveP=PALETTE.slice(),saveM=Object.assign({},MAP),saveU=JSON.parse(JSON.stringify(UIMAP)),saveSel=selectedIdx; setSyntaxFg('bg','#0d0b0a');setSyntaxFg('p','#f0fef0'); PALETTE=[['#0d0b0a','ground'],['#f0fef0','fg']]; - regenColumn('#67809c',2,{ground:{bg:MAP['bg'],fg:MAP['p']}}).members.forEach(m=>PALETTE.push([m.hex,m.offset===0?'blue':'blue'+(m.offset>0?'+'+m.offset:m.offset)])); + regenColumn('#67809c',2,{ground:groundPair()}).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]; + const column=columnsFromPalette(PALETTE,groundPair()).columns[0]; A(column&&column.members.some(m=>m.hex.toLowerCase()==='#3a8a8a'),'column base recolored to the new hex'); A(column&&column.members.length===5,'count preserved (±2 → 5 members), got '+(column&&column.members.length)); A(!new Set(PALETTE.map(p=>p[0].toLowerCase())).has('#67809c'),'old base removed from palette'); @@ -3365,7 +3369,7 @@ if(location.hash==='#paltoggletest'){let ok=true;const notes=[];const A=(c,n)=>{ const saveP=PALETTE.slice(),saveM=Object.assign({},MAP); setSyntaxFg('bg','#101010');setSyntaxFg('p','#f0f0f0'); PALETTE=[['#101010','bg','ground'],['#f0f0f0','fg','ground']]; - regenColumn('#67809c',2,{ground:{bg:MAP['bg'],fg:MAP['p']}}).members.forEach(m=>PALETTE.push([m.hex,m.offset===0?'blue':'blue'+(m.offset>0?'+'+m.offset:m.offset),'blue'])); + regenColumn('#67809c',2,{ground:groundPair()}).members.forEach(m=>PALETTE.push([m.hex,m.offset===0?'blue':'blue'+(m.offset>0?'+'+m.offset:m.offset),'blue'])); renderPalette(); const tg=document.getElementById('paltoggle'); A(!!tg,'palette-toggle-present'); |
