diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-09 20:00:27 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-09 20:00:27 -0500 |
| commit | 0c56d76c6e1bff97e52a60d27b783a664fefc24c (patch) | |
| tree | 48de70a5bc1110366fc57988d7b3b067b47021fb | |
| parent | 377e8d2a0bbf9119bd1df7f749d1d728c6cdf6c0 (diff) | |
| download | dotemacs-0c56d76c6e1bff97e52a60d27b783a664fefc24c.tar.gz dotemacs-0c56d76c6e1bff97e52a60d27b783a664fefc24c.zip | |
feat(theme-studio): warn which ramp steps collide with existing names
A ramp step whose name already exists in the palette is skipped on add, but the only signal was a count. Now preview marks each colliding tile with a dashed outline and a badge, and the message names every collision, so you can see which steps won't add before you add them. Add-all reports the skipped names too, not just how many. The single-tile add already named its one collision; this extends the same warning to preview and add-all.
| -rw-r--r-- | scripts/theme-studio/app.js | 22 | ||||
| -rw-r--r-- | scripts/theme-studio/styles.css | 2 | ||||
| -rw-r--r-- | scripts/theme-studio/theme-studio.html | 24 |
3 files changed, 36 insertions, 12 deletions
diff --git a/scripts/theme-studio/app.js b/scripts/theme-studio/app.js index ff6d7c47..44a2ee74 100644 --- a/scripts/theme-studio/app.js +++ b/scripts/theme-studio/app.js @@ -301,16 +301,22 @@ function closeRamp(){const r=document.getElementById('ramp');if(r)r.style.displa function rampOpts(){return {n:parseInt(document.getElementById('rampn').value,10),stepL:parseFloat(document.getElementById('rampstepl').value),chromaEase:parseFloat(document.getElementById('rampce').value)};} function rampStepName(off){return rampBase.name+(off>0?'+'+off:String(off));} function rampNote(msg,err){const m=document.getElementById('rampmsg');if(!m)return;m.textContent=msg||'';m.style.color=err?'#cb6b4d':'#8a9496';} +function rampNameTaken(nm){return PALETTE.some(p=>p[1].toLowerCase()===nm.toLowerCase());} function renderRamp(){ rampBase=rampBaseFromTile(); document.getElementById('rampname').textContent=rampBase.name+' '+rampBase.hex; const r=ramp(rampBase.hex,rampOpts()),prev=document.getElementById('rampprev');prev.innerHTML=''; if(r.error){rampNote('not a valid base color',true);return;} - rampNote(r.adjusted.length?('adjusted: '+r.adjusted.join(', ')):'',false); - r.steps.forEach(s=>{const nm=rampStepName(s.offset);const c=document.createElement('div');c.className='rchip';c.style.background=s.hex;c.style.color=textOn(s.hex); - c.title=nm+' '+s.hex+(s.clamped?' (gamut-clamped)':''); - c.innerHTML=`<span>${esc(nm)}</span><span class="rhex">${s.hex}</span>${s.clamped?'<span class="rclamp" title="clamped to sRGB">!</span>':''}`; + const dups=[]; + r.steps.forEach(s=>{const nm=rampStepName(s.offset),taken=rampNameTaken(nm);if(taken)dups.push(nm); + const c=document.createElement('div');c.className='rchip'+(taken?' dup':'');c.style.background=s.hex;c.style.color=textOn(s.hex); + c.title=nm+' '+s.hex+(s.clamped?' (gamut-clamped)':'')+(taken?' — a palette color is already named this; it will be skipped on add':''); + c.innerHTML=`<span>${esc(nm)}</span><span class="rhex">${s.hex}</span>${s.clamped?'<span class="rclamp" title="clamped to sRGB">!</span>':''}${taken?'<span class="rdup" title="name already in the palette">⊘</span>':''}`; c.onclick=()=>addRampStep(s);prev.appendChild(c);}); + const parts=[]; + if(r.adjusted.length)parts.push('adjusted: '+r.adjusted.join(', ')); + if(dups.length)parts.push('name already in palette, will be skipped on add: '+dups.join(', ')); + rampNote(parts.join(' | '),dups.length>0); } // Insert a step adjacent to the source swatch, keeping the ramp siblings in // -n..+n order. A name collision is flagged and skipped (never overwrites); a @@ -331,8 +337,8 @@ function addRampStep(s){ function addAllRampSteps(){ if(!rampBase)return;const r=ramp(rampBase.hex,rampOpts()); if(r.error){rampNote('not a valid base color',true);return;} - let added=0,skipped=0;r.steps.forEach(s=>{addRampStep(s)?added++:skipped++;}); - rampNote('added '+added+(skipped?(', skipped '+skipped+' (name exists)'):''),false); + let added=0;const skipped=[];r.steps.forEach(s=>{addRampStep(s)?added++:skipped.push(rampStepName(s.offset));}); + rampNote('added '+added+(skipped.length?(' | skipped (name already in palette): '+skipped.join(', ')):''),skipped.length>0); } function themeName(){return (document.getElementById('themename').value||'theme').trim()||'theme';} function fileSlug(){return slugify(themeName());} @@ -1017,6 +1023,10 @@ if(location.hash==='#ramptest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c const names=PALETTE.map(p=>p[1]),bi=names.indexOf('blue'); A(names.slice(bi,bi+5).join(',')==='blue,blue-2,blue-1,blue+1,blue+2','order after blue: '+names.slice(bi,bi+5).join(',')); const before=PALETTE.length;addAllRampSteps();A(PALETTE.length===before,'re-add should skip existing names'); + A(/skipped \(name already in palette\): blue-2, blue-1, blue\+1, blue\+2/.test(document.getElementById('rampmsg').textContent),'add-all names the skipped collisions: '+document.getElementById('rampmsg').textContent); + renderRamp(); + A(document.querySelectorAll('#rampprev .rchip.dup').length===4,'re-preview marks the now-existing names as dup'); + A(/already in palette.*blue-2, blue-1, blue\+1, blue\+2/.test(document.getElementById('rampmsg').textContent),'preview names the colliding tiles: '+document.getElementById('rampmsg').textContent); // preview re-reads the color-selection tile: change the tile, press preview, the base follows document.getElementById('newhexstr').value='#2040e0';document.getElementById('newname').value='vivid';selectedIdx=null;document.getElementById('rampce').value='0';renderRamp(); A(/^vivid #2040e0/.test(document.getElementById('rampname').textContent),'preview reads the tile: '+document.getElementById('rampname').textContent); diff --git a/scripts/theme-studio/styles.css b/scripts/theme-studio/styles.css index f2fc6b4b..79a7efe2 100644 --- a/scripts/theme-studio/styles.css +++ b/scripts/theme-studio/styles.css @@ -69,6 +69,8 @@ .rchip{width:128px;height:48px;border-radius:5px;border:1px solid #555;position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:pointer;font:bold 9pt monospace;line-height:1.3} .rchip .rhex{font-weight:normal;font-size:8pt;opacity:.85} .rchip .rclamp{position:absolute;top:2px;right:4px;color:#cb6b4d;font-weight:bold;font-size:12px} + .rchip.dup{outline:2px dashed #e8bd30;outline-offset:-2px} + .rchip .rdup{position:absolute;top:2px;left:4px;color:#e8bd30;font-weight:bold;font-size:12px} #rampmsg{font:10pt monospace;margin-top:6px;min-height:14px;color:#8a9496} .svsafe{position:absolute;left:0;width:100%;background:rgba(203,107,77,0.30);border-bottom:2px solid #cb6b4d;pointer-events:none;z-index:2} .palctl button,.filebar button,.fbtn{background:#252321;color:#e8bd30;border:1px solid #3a3a3a;border-radius:4px;padding:6px 12px;font:10pt monospace;cursor:pointer} diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html index 39a2a2ae..a9b030f9 100644 --- a/scripts/theme-studio/theme-studio.html +++ b/scripts/theme-studio/theme-studio.html @@ -71,6 +71,8 @@ .rchip{width:128px;height:48px;border-radius:5px;border:1px solid #555;position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:pointer;font:bold 9pt monospace;line-height:1.3} .rchip .rhex{font-weight:normal;font-size:8pt;opacity:.85} .rchip .rclamp{position:absolute;top:2px;right:4px;color:#cb6b4d;font-weight:bold;font-size:12px} + .rchip.dup{outline:2px dashed #e8bd30;outline-offset:-2px} + .rchip .rdup{position:absolute;top:2px;left:4px;color:#e8bd30;font-weight:bold;font-size:12px} #rampmsg{font:10pt monospace;margin-top:6px;min-height:14px;color:#8a9496} .svsafe{position:absolute;left:0;width:100%;background:rgba(203,107,77,0.30);border-bottom:2px solid #cb6b4d;pointer-events:none;z-index:2} .palctl button,.filebar button,.fbtn{background:#252321;color:#e8bd30;border:1px solid #3a3a3a;border-radius:4px;padding:6px 12px;font:10pt monospace;cursor:pointer} @@ -825,16 +827,22 @@ function closeRamp(){const r=document.getElementById('ramp');if(r)r.style.displa function rampOpts(){return {n:parseInt(document.getElementById('rampn').value,10),stepL:parseFloat(document.getElementById('rampstepl').value),chromaEase:parseFloat(document.getElementById('rampce').value)};} function rampStepName(off){return rampBase.name+(off>0?'+'+off:String(off));} function rampNote(msg,err){const m=document.getElementById('rampmsg');if(!m)return;m.textContent=msg||'';m.style.color=err?'#cb6b4d':'#8a9496';} +function rampNameTaken(nm){return PALETTE.some(p=>p[1].toLowerCase()===nm.toLowerCase());} function renderRamp(){ rampBase=rampBaseFromTile(); document.getElementById('rampname').textContent=rampBase.name+' '+rampBase.hex; const r=ramp(rampBase.hex,rampOpts()),prev=document.getElementById('rampprev');prev.innerHTML=''; if(r.error){rampNote('not a valid base color',true);return;} - rampNote(r.adjusted.length?('adjusted: '+r.adjusted.join(', ')):'',false); - r.steps.forEach(s=>{const nm=rampStepName(s.offset);const c=document.createElement('div');c.className='rchip';c.style.background=s.hex;c.style.color=textOn(s.hex); - c.title=nm+' '+s.hex+(s.clamped?' (gamut-clamped)':''); - c.innerHTML=`<span>${esc(nm)}</span><span class="rhex">${s.hex}</span>${s.clamped?'<span class="rclamp" title="clamped to sRGB">!</span>':''}`; + const dups=[]; + r.steps.forEach(s=>{const nm=rampStepName(s.offset),taken=rampNameTaken(nm);if(taken)dups.push(nm); + const c=document.createElement('div');c.className='rchip'+(taken?' dup':'');c.style.background=s.hex;c.style.color=textOn(s.hex); + c.title=nm+' '+s.hex+(s.clamped?' (gamut-clamped)':'')+(taken?' — a palette color is already named this; it will be skipped on add':''); + c.innerHTML=`<span>${esc(nm)}</span><span class="rhex">${s.hex}</span>${s.clamped?'<span class="rclamp" title="clamped to sRGB">!</span>':''}${taken?'<span class="rdup" title="name already in the palette">⊘</span>':''}`; c.onclick=()=>addRampStep(s);prev.appendChild(c);}); + const parts=[]; + if(r.adjusted.length)parts.push('adjusted: '+r.adjusted.join(', ')); + if(dups.length)parts.push('name already in palette, will be skipped on add: '+dups.join(', ')); + rampNote(parts.join(' | '),dups.length>0); } // Insert a step adjacent to the source swatch, keeping the ramp siblings in // -n..+n order. A name collision is flagged and skipped (never overwrites); a @@ -855,8 +863,8 @@ function addRampStep(s){ function addAllRampSteps(){ if(!rampBase)return;const r=ramp(rampBase.hex,rampOpts()); if(r.error){rampNote('not a valid base color',true);return;} - let added=0,skipped=0;r.steps.forEach(s=>{addRampStep(s)?added++:skipped++;}); - rampNote('added '+added+(skipped?(', skipped '+skipped+' (name exists)'):''),false); + let added=0;const skipped=[];r.steps.forEach(s=>{addRampStep(s)?added++:skipped.push(rampStepName(s.offset));}); + rampNote('added '+added+(skipped.length?(' | skipped (name already in palette): '+skipped.join(', ')):''),skipped.length>0); } function themeName(){return (document.getElementById('themename').value||'theme').trim()||'theme';} function fileSlug(){return slugify(themeName());} @@ -1541,6 +1549,10 @@ if(location.hash==='#ramptest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c const names=PALETTE.map(p=>p[1]),bi=names.indexOf('blue'); A(names.slice(bi,bi+5).join(',')==='blue,blue-2,blue-1,blue+1,blue+2','order after blue: '+names.slice(bi,bi+5).join(',')); const before=PALETTE.length;addAllRampSteps();A(PALETTE.length===before,'re-add should skip existing names'); + A(/skipped \(name already in palette\): blue-2, blue-1, blue\+1, blue\+2/.test(document.getElementById('rampmsg').textContent),'add-all names the skipped collisions: '+document.getElementById('rampmsg').textContent); + renderRamp(); + A(document.querySelectorAll('#rampprev .rchip.dup').length===4,'re-preview marks the now-existing names as dup'); + A(/already in palette.*blue-2, blue-1, blue\+1, blue\+2/.test(document.getElementById('rampmsg').textContent),'preview names the colliding tiles: '+document.getElementById('rampmsg').textContent); // preview re-reads the color-selection tile: change the tile, press preview, the base follows document.getElementById('newhexstr').value='#2040e0';document.getElementById('newname').value='vivid';selectedIdx=null;document.getElementById('rampce').value='0';renderRamp(); A(/^vivid #2040e0/.test(document.getElementById('rampname').textContent),'preview reads the tile: '+document.getElementById('rampname').textContent); |
