From 603e064e8595bea103c0df3939aa4a4b006309f3 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sat, 13 Jun 2026 17:38:01 -0500 Subject: Make theme studio column delete safer --- scripts/theme-studio/README.md | 9 +++++---- scripts/theme-studio/browser-gates.js | 8 +++++++- scripts/theme-studio/palette-actions.js | 6 ++++-- scripts/theme-studio/styles.css | 2 ++ scripts/theme-studio/theme-studio.html | 16 +++++++++++++--- 5 files changed, 31 insertions(+), 10 deletions(-) (limited to 'scripts/theme-studio') diff --git a/scripts/theme-studio/README.md b/scripts/theme-studio/README.md index a357a61f..7e8fda54 100644 --- a/scripts/theme-studio/README.md +++ b/scripts/theme-studio/README.md @@ -129,10 +129,11 @@ 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. +- **Deleting.** Normal columns have a separated header delete control with a + confirmation prompt. Confirming 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/browser-gates.js b/scripts/theme-studio/browser-gates.js index eceb11de..cc1b3aaa 100644 --- a/scripts/theme-studio/browser-gates.js +++ b/scripts/theme-studio/browser-gates.js @@ -336,8 +336,14 @@ if(location.hash==='#columntest'||location.hash==='#familytest'){let ok=true;con 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'); + A(!!del,'normal column has a delete button'); + const beforeDelete=PALETTE.map(p=>p.join('|')).join('||'),oldConfirm=window.confirm; + window.confirm=()=>false; if(del)del.click(); + A(PALETTE.map(p=>p.join('|')).join('||')===beforeDelete,'canceling column delete leaves the palette unchanged'); + window.confirm=()=>true; + if(del)del.click(); + 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'); diff --git a/scripts/theme-studio/palette-actions.js b/scripts/theme-studio/palette-actions.js index e4993a60..a4aef83c 100644 --- a/scripts/theme-studio/palette-actions.js +++ b/scripts/theme-studio/palette-actions.js @@ -122,15 +122,17 @@ 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;} + 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; 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); + notify('deleted column "'+title+'" — '+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=``; + h.innerHTML=``; h.querySelector('.ctitle').textContent=label; h.querySelector('.ctitle').onclick=()=>selectColumnBase(f); h.querySelector('.left').onclick=(e)=>{e.stopPropagation();moveColumn(f.column,-1);}; diff --git a/scripts/theme-studio/styles.css b/scripts/theme-studio/styles.css index 14480b54..8285ba99 100644 --- a/scripts/theme-studio/styles.css +++ b/scripts/theme-studio/styles.css @@ -32,6 +32,8 @@ .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} + .fhead .cdel{margin-left:5px;color:#b36a5e} + .fhead .cdel:hover{color:#ff9078;border-color:#6d342c;background:#211512} .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} .palwarn{display:none;margin-top:8px;font:10pt monospace;color:#cb6b4d} diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html index 5a9653ea..4c78026c 100644 --- a/scripts/theme-studio/theme-studio.html +++ b/scripts/theme-studio/theme-studio.html @@ -34,6 +34,8 @@ .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} + .fhead .cdel{margin-left:5px;color:#b36a5e} + .fhead .cdel:hover{color:#ff9078;border-color:#6d342c;background:#211512} .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} .palwarn{display:none;margin-top:8px;font:10pt monospace;color:#cb6b4d} @@ -987,15 +989,17 @@ 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;} + 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; 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); + notify('deleted column "'+title+'" — '+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=``; + h.innerHTML=``; h.querySelector('.ctitle').textContent=label; h.querySelector('.ctitle').onclick=()=>selectColumnBase(f); h.querySelector('.left').onclick=(e)=>{e.stopPropagation();moveColumn(f.column,-1);}; @@ -2075,8 +2079,14 @@ if(location.hash==='#columntest'||location.hash==='#familytest'){let ok=true;con 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'); + A(!!del,'normal column has a delete button'); + const beforeDelete=PALETTE.map(p=>p.join('|')).join('||'),oldConfirm=window.confirm; + window.confirm=()=>false; if(del)del.click(); + A(PALETTE.map(p=>p.join('|')).join('||')===beforeDelete,'canceling column delete leaves the palette unchanged'); + window.confirm=()=>true; + if(del)del.click(); + 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'); -- cgit v1.2.3