From 7e7b871fe4f8daff724c3df37feb5572464532c1 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 15 Jun 2026 19:19:38 -0500 Subject: feat(theme-studio): flag unused palette tiles and columns I added usedPaletteHexes, a reverse lookup over the syntax, ui, and package assignments (plus the ground endpoints) that resolves each reference to a hex. renderPalette outlines a tile whose color is referenced nowhere and outlines a whole column when none of its colors are used, so dead colors stand out for pruning before a theme ships. The check is biased safe: an unresolvable reference marks nothing, so a color that is actually used is never flagged. Node tests cover the lookup. A #unusedtest gate covers the tile and column flags. --- scripts/theme-studio/theme-studio.html | 47 +++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) (limited to 'scripts/theme-studio/theme-studio.html') diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html index e30dda001..5813342e9 100644 --- a/scripts/theme-studio/theme-studio.html +++ b/scripts/theme-studio/theme-studio.html @@ -49,6 +49,8 @@ .cat{color:#b4b1a2} .ex{font-size:17px} .crerr{display:inline-block;margin-left:8px;padding:0 4px;border-radius:3px;background:#2b130e;color:#cb6b4d;border:1px solid #7b3324;font:9pt monospace;vertical-align:middle} .paltoggle{align-self:flex-start;width:22px;height:22px;padding:0;border:1px solid #3a3a3a;border-radius:4px;background:#1f1c19;color:#e8bd30;font:12px monospace;line-height:1;cursor:pointer;margin-right:6px} + .pchip.unused{outline:2px dashed #cb6b4d;outline-offset:-2px} + .fstrip.unused-col{outline:1px dashed #cb6b4d;outline-offset:2px;border-radius:8px} .sbtn{width:17px;height:15px;border:1px solid #3a3a3a;border-radius:3px;background:#eaeaea;color:#111;cursor:pointer;font-size:13px;line-height:1;margin-right:2px;padding:0} .sbtn.on{background:#0d0b0a;color:#cdced1;border-color:#8a9496} .pals{display:flex;flex-direction:row;flex-wrap:wrap;gap:10px;align-items:flex-start} @@ -734,6 +736,21 @@ function toggleLockSet(keys,locked){ // from the palette, and ground+N entries are reserved for that column. Everything // else groups by its stable column id, not by OKLCH hue/chroma or display name. // Legacy two-field entries fall back to their generated-name stem until edited. +// Reverse lookup: every palette hex referenced by an assignment (syntax, ui, or +// package fg / bg / box-color), plus the ground endpoints, which are always in +// use. Values may be palette names or hexes; nameToHex resolves both, so a tile +// whose hex is absent from this set is genuinely unreferenced. Biased safe: an +// unresolvable value simply marks nothing, so a used color is never flagged. +function usedPaletteHexes(palette,syntax,uimap,pkgmap,ground){ + const used=new Set(); + const add=v=>{const h=nameToHex(v,palette);if(h)used.add(h.toLowerCase());}; + const addFace=f=>{if(!f)return;add(f.fg);add(f.bg);if(f.box&&f.box.color)add(f.box.color);}; + if(ground){if(ground.bg)add(ground.bg);if(ground.fg)add(ground.fg);} + for(const k in (syntax||{}))addFace(syntax[k]); + for(const face in (uimap||{}))addFace(uimap[face]); + for(const app in (pkgmap||{}))for(const face in pkgmap[app])addFace(pkgmap[app][face]); + return used; +} function columnsFromPalette(palette,ground){ const bg=ground&&ground.bg,fg=ground&&ground.fg; const groundStrip=[]; @@ -1542,13 +1559,14 @@ function renderPaletteWarnings(warnings,overflow){ } // 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){ +function paletteChip(i,nearest,used){ 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.dataset.paletteIndex=String(i); d.title=name+' '+hex+(nde===Infinity||nde===undefined?'':' — nearest ΔE '+nde.toFixed(3)); + if(used&&!used.has(hex.toLowerCase())){d.classList.add('unused');d.title+=' — not used in the theme';} const rm=locked?`🔒`:``; d.innerHTML=`${rm}
${hex}
`; if(!locked)d.querySelector('.rm').onclick=(e)=>{e.stopPropagation();rememberGone(hex,name);PALETTE.splice(i,1);if(selectedIdx===i)selectedIdx=null;refreshPaletteState({code:false,ground:false});}; @@ -1638,6 +1656,7 @@ function 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 used=new Set(); const idxOf=(hex,name)=>{for(let i=0;i{const s=document.createElement('div');s.className='fstrip'+(cls||'');p.appendChild(s);return s;}; @@ -1647,7 +1666,7 @@ function renderPalette(){ gs.appendChild(groundSpanControl()); (paletteShowFull?groundColumnMembers():groundColumnMembers().filter(m=>!/^ground[+-]\d+$/i.test(m.name||''))).forEach(m=>{ const i=idxOf(m.hex,m.name); - if(i>=0)gs.appendChild(paletteChip(i,nearest)); + if(i>=0)gs.appendChild(paletteChip(i,nearest,usedHexes)); 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=`
${m.hex}
`;gs.appendChild(sw);} }); @@ -1659,7 +1678,8 @@ function renderPalette(){ const s=strip('');s.dataset.column=f.column||f.base; s.appendChild(columnHeader(f,pos,ordered.length)); s.appendChild(columnCountControl(f)); - (paletteShowFull?f.members:f.members.filter(m=>m.hex.toLowerCase()===f.base.toLowerCase())).forEach(m=>{const i=idxOf(m.hex,m.name);if(i>=0)s.appendChild(paletteChip(i,nearest));}); + (paletteShowFull?f.members:f.members.filter(m=>m.hex.toLowerCase()===f.base.toLowerCase())).forEach(m=>{const i=idxOf(m.hex,m.name);if(i>=0)s.appendChild(paletteChip(i,nearest,usedHexes));}); + if(f.members.every(m=>!usedHexes.has(m.hex.toLowerCase())))s.classList.add('unused-col'); }); renderPaletteWarnings(warnings,overflow); buildUITable();if(document.getElementById('pkgbody'))buildPkgTable(); @@ -3251,4 +3271,25 @@ if(location.hash==='#paltoggletest'){let ok=true;const notes=[];const A=(c,n)=>{ PALETTE=saveP;for(const k in MAP)delete MAP[k];Object.assign(MAP,saveM);syncSyntaxFromCache();renderPalette(); document.title='PALTOGGLETEST '+(ok?'PASS':'FAIL'); const d=document.createElement('div');d.id='paltoggletest';d.textContent='PALTOGGLETEST '+(ok?'PASS':'FAIL')+(notes.length?' fails='+notes.join(','):'');document.body.appendChild(d);} +// Unused-tile gate (open with #unusedtest): a palette color referenced nowhere +// in the theme gets the .unused flag; a column with no used members gets +// .unused-col; referenced colors stay unflagged. +if(location.hash==='#unusedtest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}}; + const saveP=PALETTE.slice(),saveM=Object.assign({},MAP),saveSyn=JSON.parse(JSON.stringify(SYNTAX)),saveU=JSON.parse(JSON.stringify(UIMAP)); + setSyntaxFg('bg','#101010');setSyntaxFg('p','#f0f0f0'); + PALETTE=[['#101010','bg','ground'],['#f0f0f0','fg','ground'],['#67809c','blue','blue'],['#123456','teal','teal']]; + for(const f in UIMAP)UIMAP[f]={fg:null,bg:null,bold:false,italic:false,underline:false,strike:false}; + setSyntaxFg('kw','#67809c'); + renderPalette(); + const tealStrip=document.querySelector('#pals .fstrip[data-column="teal"]'); + const blueStrip=document.querySelector('#pals .fstrip[data-column="blue"]'); + const tealChip=tealStrip&&tealStrip.querySelector('.pchip'); + const blueChip=blueStrip&&blueStrip.querySelector('.pchip'); + A(tealChip&&tealChip.classList.contains('unused'),'unreferenced-tile-flagged'); + A(blueChip&&!blueChip.classList.contains('unused'),'referenced-tile-not-flagged'); + A(tealStrip&&tealStrip.classList.contains('unused-col'),'all-unused-column-flagged'); + A(blueStrip&&!blueStrip.classList.contains('unused-col'),'used-column-not-flagged'); + PALETTE=saveP;for(const k in MAP)delete MAP[k];Object.assign(MAP,saveM);for(const k in SYNTAX)delete SYNTAX[k];Object.assign(SYNTAX,saveSyn);for(const f in UIMAP)delete UIMAP[f];Object.assign(UIMAP,saveU);syncSyntaxFromCache();renderPalette(); + document.title='UNUSEDTEST '+(ok?'PASS':'FAIL'); + const d=document.createElement('div');d.id='unusedtest';d.textContent='UNUSEDTEST '+(ok?'PASS':'FAIL')+(notes.length?' fails='+notes.join(','):'');document.body.appendChild(d);} -- cgit v1.2.3