aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rw-r--r--scripts/theme-studio/README.md4
-rw-r--r--scripts/theme-studio/app-core.js11
-rw-r--r--scripts/theme-studio/browser-gates.js12
-rw-r--r--scripts/theme-studio/palette-actions.js17
-rw-r--r--scripts/theme-studio/styles.css4
-rw-r--r--scripts/theme-studio/test-app-core.mjs25
-rw-r--r--scripts/theme-studio/theme-studio.html42
7 files changed, 103 insertions, 12 deletions
diff --git a/scripts/theme-studio/README.md b/scripts/theme-studio/README.md
index 54a1e985..a1178d1a 100644
--- a/scripts/theme-studio/README.md
+++ b/scripts/theme-studio/README.md
@@ -129,6 +129,10 @@ derived from hue, chroma, lightness, or the visible color name.
`grey80`, `orange3`, and `orchid4` group by their text stem. Imported names
that begin with `bg` or `fg` are normal colors unless they are exact ground
endpoints or explicitly use the `ground` column id.
+- **Deleting.** Normal columns have a header delete control that removes every
+ tile in that column. The ground column is pinned and cannot be deleted. Face
+ assignments that used a deleted tile stay on that old hex and appear as
+ recoverable "(gone)" values, matching individual chip deletion.
- **The count control** under each non-ground column sets how many steps sit on
each side of the column's base. Setting N regenerates the column as a symmetric
base ±N tonal ramp via `ramp()` — lighter and darker steps on the base's hue
diff --git a/scripts/theme-studio/app-core.js b/scripts/theme-studio/app-core.js
index 5e889bed..264c7be9 100644
--- a/scripts/theme-studio/app-core.js
+++ b/scripts/theme-studio/app-core.js
@@ -189,6 +189,15 @@ function clearPalettePlan(palette,ground){
return {palette:keep,removed};
}
+function deletePaletteColumnPlan(palette,ground,columnId){
+ const normalized=palette.map(normalizePaletteEntryCore),removed=[],keep=[];
+ for(const entry of normalized){
+ if(groundRoleOfEntry(entry,ground)||columnIdOf(entry)!==columnId)keep.push(entry);
+ else removed.push({hex:entry[0],name:entry[1]});
+ }
+ return {palette:keep,removed};
+}
+
function areAllLocked(keys,locked){
const has=k=>locked instanceof Set?locked.has(k):Array.isArray(locked)&&locked.includes(k);
return !!(keys&&keys.length)&&keys.every(has);
@@ -281,4 +290,4 @@ function paletteOptionList(cur,palette,ground){
return out;
}
-export { nameToHex, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, optList, paletteOptionList, slugify, ramp, fgSetFor, floor, lMax, COVERED_FACES, columnsFromPalette, regenColumn, rankByLightness, stepRepointPlan, sortColumns, sortColumnMembers, groundRoleOfEntry, groundColumnMembersFromPalette, clearPalettePlan, areAllLocked, lockToggleLabel, toggleLockSet };
+export { nameToHex, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, optList, paletteOptionList, slugify, ramp, fgSetFor, floor, lMax, COVERED_FACES, columnsFromPalette, regenColumn, rankByLightness, stepRepointPlan, sortColumns, sortColumnMembers, groundRoleOfEntry, groundColumnMembersFromPalette, clearPalettePlan, deletePaletteColumnPlan, areAllLocked, lockToggleLabel, toggleLockSet };
diff --git a/scripts/theme-studio/browser-gates.js b/scripts/theme-studio/browser-gates.js
index 8cba692a..eceb11de 100644
--- a/scripts/theme-studio/browser-gates.js
+++ b/scripts/theme-studio/browser-gates.js
@@ -320,6 +320,7 @@ if(location.hash==='#columntest'||location.hash==='#familytest'){let ok=true;con
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');
+ A(!document.querySelector('#pals .fstrip[data-column="ground"] .cdel'),'ground column has no delete button');
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;
@@ -332,6 +333,17 @@ if(location.hash==='#columntest'||location.hash==='#familytest'){let ok=true;con
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'],['#808080','gray','gray']];
+ MAP['kw']='#92acc2';lastGone={};selectedIdx=PALETTE.findIndex(p=>p[1]==='blue+1');renderPalette();
+ const del=document.querySelector('#pals .fstrip[data-column="blue"] .cdel');
+ A(!!del,'normal column has a delete button');
+ if(del)del.click();
+ 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(MAP['kw']==='#92acc2','column delete leaves assignments on removed hexes');
+ A(lastGone['blue']==='#3a6ea5'&&lastGone['blue+1']==='#92acc2','column delete records every removed name for recovery');
+ 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']];
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');
diff --git a/scripts/theme-studio/palette-actions.js b/scripts/theme-studio/palette-actions.js
index ccbf3431..e4993a60 100644
--- a/scripts/theme-studio/palette-actions.js
+++ b/scripts/theme-studio/palette-actions.js
@@ -1,7 +1,7 @@
function clearPalette(){
normalizePalette();
const plan=clearPalettePlan(PALETTE,{bg:MAP['bg'],fg:MAP['p']});
- plan.removed.forEach(({hex,name})=>{lastGone[name.toLowerCase()]=hex;});
+ plan.removed.forEach(({hex,name})=>rememberGone(hex,name));
PALETTE=plan.palette;selectedIdx=null;
renderPalette();buildTable();buildUITable();if(document.getElementById('pkgbody'))buildPkgTable();renderCode();applyGround();
notify('cleared palette to bg and fg',false);
@@ -23,6 +23,7 @@ function repointHex(oldHex,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 rememberGone(hex,name){if(name)lastGone[name.toLowerCase()]=hex;}
function normalizePaletteEntry(entry){
const hex=entry&&entry[0],name=(entry&&entry[1])||'color';
return [hex,name,(entry&&entry[2])||columnIdOf(entry)];
@@ -81,7 +82,7 @@ function paletteChip(i,nearest){
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();};
+ if(!locked)d.querySelector('.rm').onclick=(e)=>{e.stopPropagation();rememberGone(hex,name);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;
@@ -117,14 +118,24 @@ function moveColumn(columnId,dir){
selectedIdx=null;renderPalette();buildTable();buildUITable();renderCode();applyGround();
notify('moved "'+columnId+'" '+(dir<0?'left':'right'),false);
}
+function deleteColumn(columnId,label){
+ normalizePalette();
+ const plan=deletePaletteColumnPlan(PALETTE,{bg:MAP['bg'],fg:MAP['p']},columnId);
+ if(!plan.removed.length){notify('nothing to delete in "'+(label||columnId)+'"',true);return;}
+ plan.removed.forEach(({hex,name})=>rememberGone(hex,name));
+ PALETTE=plan.palette;selectedIdx=null;
+ renderPalette();buildTable();buildUITable();renderCode();applyGround();
+ notify('deleted column "'+(label||columnId)+'" — '+plan.removed.length+' color(s) now show "(gone)" where used',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.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><button class="cdel" title="delete column">×</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);};
+ h.querySelector('.cdel').onclick=(e)=>{e.stopPropagation();deleteColumn(f.column,label);};
return h;
}
// Render the palette as structural color columns: pinned ground column, then
diff --git a/scripts/theme-studio/styles.css b/scripts/theme-studio/styles.css
index 7367d096..14480b54 100644
--- a/scripts/theme-studio/styles.css
+++ b/scripts/theme-studio/styles.css
@@ -29,8 +29,8 @@
.fhead{min-height:17px;width:128px;display:flex;align-items:center;justify-content:center;gap:3px;color:#b4b1a2;font:9pt monospace;text-align:center}
.fhead .ctitle{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;background:none;border:none;color:#b4b1a2;font:9pt monospace;text-align:center;cursor:pointer;padding:0}
.fhead .ctitle:hover{color:#e8bd30}
- .fhead .cmove{width:18px;height:17px;background:#161412;border:1px solid #252321;border-radius:3px;color:#8a9496;font:10pt monospace;line-height:13px;padding:0;cursor:pointer}
- .fhead .cmove:hover:not(:disabled){color:#e8bd30;border-color:#3a3a3a}
+ .fhead .cmove,.fhead .cdel{width:18px;height:17px;background:#161412;border:1px solid #252321;border-radius:3px;color:#8a9496;font:10pt monospace;line-height:13px;padding:0;cursor:pointer}
+ .fhead .cmove:hover:not(:disabled),.fhead .cdel:hover{color:#e8bd30;border-color:#3a3a3a}
.fhead .cmove:disabled{opacity:.28;cursor:default}
.fcount{margin-top:3px;font:9pt monospace;color:#8a9496;text-align:center}
.fcount input{width:40px;background:#0d0b0a;border:1px solid #252321;color:#cdced1;border-radius:4px;padding:2px 4px;font:9pt monospace;text-align:center}
diff --git a/scripts/theme-studio/test-app-core.mjs b/scripts/theme-studio/test-app-core.mjs
index 47aaa09f..059a50b2 100644
--- a/scripts/theme-studio/test-app-core.mjs
+++ b/scripts/theme-studio/test-app-core.mjs
@@ -8,7 +8,7 @@ import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import {
nameToHex, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, optList, paletteOptionList, slugify,
- clearPalettePlan, groundColumnMembersFromPalette, areAllLocked, lockToggleLabel, toggleLockSet,
+ clearPalettePlan, deletePaletteColumnPlan, groundColumnMembersFromPalette, areAllLocked, lockToggleLabel, toggleLockSet,
} from './app-core.js';
const here = fileURLToPath(new URL('.', import.meta.url));
@@ -104,6 +104,29 @@ test('clearPalettePlan: Boundary — same-hex imported colors are not ground end
assert.deepEqual(plan.removed, [{ hex: '#0d0b0a', name: 'bg2' }]);
});
+test('deletePaletteColumnPlan: Normal — removes one stable column and keeps ground plus neighbors', () => {
+ const plan = deletePaletteColumnPlan([
+ ['#0d0b0a', 'bg', 'ground'],
+ ['#f0fef0', 'fg', 'ground'],
+ ['#c0402a', 'red', 'red'],
+ ['#3a6ea5', 'blue', 'blue'],
+ ['#92acc2', 'blue+1', 'blue'],
+ ['#808080', 'gray', 'gray'],
+ ], { bg: '#0d0b0a', fg: '#f0fef0' }, 'blue');
+ assert.deepEqual(plan.palette.map(p => p[1]), ['bg', 'fg', 'red', 'gray']);
+ assert.deepEqual(plan.removed, [{ hex: '#3a6ea5', name: 'blue' }, { hex: '#92acc2', name: 'blue+1' }]);
+});
+
+test('deletePaletteColumnPlan: Boundary — never deletes ground entries', () => {
+ const plan = deletePaletteColumnPlan([
+ ['#0d0b0a', 'bg', 'ground'],
+ ['#555555', 'ground-1', 'ground'],
+ ['#f0fef0', 'fg', 'ground'],
+ ], { bg: '#0d0b0a', fg: '#f0fef0' }, 'ground');
+ assert.deepEqual(plan.palette.map(p => p[1]), ['bg', 'ground-1', 'fg']);
+ assert.deepEqual(plan.removed, []);
+});
+
test('groundColumnMembersFromPalette: Normal — sorts bg, ground-N steps, then fg', () => {
const members = groundColumnMembersFromPalette([
['#ffffff', 'bg', 'ground'],
diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html
index 9f544176..90430a5b 100644
--- a/scripts/theme-studio/theme-studio.html
+++ b/scripts/theme-studio/theme-studio.html
@@ -31,8 +31,8 @@
.fhead{min-height:17px;width:128px;display:flex;align-items:center;justify-content:center;gap:3px;color:#b4b1a2;font:9pt monospace;text-align:center}
.fhead .ctitle{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;background:none;border:none;color:#b4b1a2;font:9pt monospace;text-align:center;cursor:pointer;padding:0}
.fhead .ctitle:hover{color:#e8bd30}
- .fhead .cmove{width:18px;height:17px;background:#161412;border:1px solid #252321;border-radius:3px;color:#8a9496;font:10pt monospace;line-height:13px;padding:0;cursor:pointer}
- .fhead .cmove:hover:not(:disabled){color:#e8bd30;border-color:#3a3a3a}
+ .fhead .cmove,.fhead .cdel{width:18px;height:17px;background:#161412;border:1px solid #252321;border-radius:3px;color:#8a9496;font:10pt monospace;line-height:13px;padding:0;cursor:pointer}
+ .fhead .cmove:hover:not(:disabled),.fhead .cdel:hover{color:#e8bd30;border-color:#3a3a3a}
.fhead .cmove:disabled{opacity:.28;cursor:default}
.fcount{margin-top:3px;font:9pt monospace;color:#8a9496;text-align:center}
.fcount input{width:40px;background:#0d0b0a;border:1px solid #252321;color:#cdced1;border-radius:4px;padding:2px 4px;font:9pt monospace;text-align:center}
@@ -607,6 +607,15 @@ function clearPalettePlan(palette,ground){
return {palette:keep,removed};
}
+function deletePaletteColumnPlan(palette,ground,columnId){
+ const normalized=palette.map(normalizePaletteEntryCore),removed=[],keep=[];
+ for(const entry of normalized){
+ if(groundRoleOfEntry(entry,ground)||columnIdOf(entry)!==columnId)keep.push(entry);
+ else removed.push({hex:entry[0],name:entry[1]});
+ }
+ return {palette:keep,removed};
+}
+
function areAllLocked(keys,locked){
const has=k=>locked instanceof Set?locked.has(k):Array.isArray(locked)&&locked.includes(k);
return !!(keys&&keys.length)&&keys.every(has);
@@ -857,7 +866,7 @@ function buildTable(){
function clearPalette(){
normalizePalette();
const plan=clearPalettePlan(PALETTE,{bg:MAP['bg'],fg:MAP['p']});
- plan.removed.forEach(({hex,name})=>{lastGone[name.toLowerCase()]=hex;});
+ plan.removed.forEach(({hex,name})=>rememberGone(hex,name));
PALETTE=plan.palette;selectedIdx=null;
renderPalette();buildTable();buildUITable();if(document.getElementById('pkgbody'))buildPkgTable();renderCode();applyGround();
notify('cleared palette to bg and fg',false);
@@ -879,6 +888,7 @@ function repointHex(oldHex,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 rememberGone(hex,name){if(name)lastGone[name.toLowerCase()]=hex;}
function normalizePaletteEntry(entry){
const hex=entry&&entry[0],name=(entry&&entry[1])||'color';
return [hex,name,(entry&&entry[2])||columnIdOf(entry)];
@@ -937,7 +947,7 @@ function paletteChip(i,nearest){
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();};
+ if(!locked)d.querySelector('.rm').onclick=(e)=>{e.stopPropagation();rememberGone(hex,name);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;
@@ -973,14 +983,24 @@ function moveColumn(columnId,dir){
selectedIdx=null;renderPalette();buildTable();buildUITable();renderCode();applyGround();
notify('moved "'+columnId+'" '+(dir<0?'left':'right'),false);
}
+function deleteColumn(columnId,label){
+ normalizePalette();
+ const plan=deletePaletteColumnPlan(PALETTE,{bg:MAP['bg'],fg:MAP['p']},columnId);
+ if(!plan.removed.length){notify('nothing to delete in "'+(label||columnId)+'"',true);return;}
+ plan.removed.forEach(({hex,name})=>rememberGone(hex,name));
+ PALETTE=plan.palette;selectedIdx=null;
+ renderPalette();buildTable();buildUITable();renderCode();applyGround();
+ notify('deleted column "'+(label||columnId)+'" — '+plan.removed.length+' color(s) now show "(gone)" where used',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.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><button class="cdel" title="delete column">×</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);};
+ h.querySelector('.cdel').onclick=(e)=>{e.stopPropagation();deleteColumn(f.column,label);};
return h;
}
// Render the palette as structural color columns: pinned ground column, then
@@ -2039,6 +2059,7 @@ if(location.hash==='#columntest'||location.hash==='#familytest'){let ok=true;con
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');
+ A(!document.querySelector('#pals .fstrip[data-column="ground"] .cdel'),'ground column has no delete button');
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;
@@ -2051,6 +2072,17 @@ if(location.hash==='#columntest'||location.hash==='#familytest'){let ok=true;con
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'],['#808080','gray','gray']];
+ MAP['kw']='#92acc2';lastGone={};selectedIdx=PALETTE.findIndex(p=>p[1]==='blue+1');renderPalette();
+ const del=document.querySelector('#pals .fstrip[data-column="blue"] .cdel');
+ A(!!del,'normal column has a delete button');
+ if(del)del.click();
+ 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(MAP['kw']==='#92acc2','column delete leaves assignments on removed hexes');
+ A(lastGone['blue']==='#3a6ea5'&&lastGone['blue+1']==='#92acc2','column delete records every removed name for recovery');
+ 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']];
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');