aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio/theme-studio.html
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-15 19:54:37 -0500
committerCraig Jennings <c@cjennings.net>2026-06-15 19:54:37 -0500
commit1b4e5f88353180cf999412faa2be9e0326b78361 (patch)
tree523ffa0c8b995ef239be947b7b62b9e198de0e6b /scripts/theme-studio/theme-studio.html
parent5f6e07d7024135d86a4883439b3d04de5314916a (diff)
downloaddotemacs-1b4e5f88353180cf999412faa2be9e0326b78361.tar.gz
dotemacs-1b4e5f88353180cf999412faa2be9e0326b78361.zip
feat(theme-studio): show view-area > element usages on palette tile hover
I added paletteUsages, which enumerates every place a color is assigned, grouped by view area (the view dropdown's names: color/code assignments, ui faces, each package app) and the element within it. renderPalette builds the per-area scopes once and appends the list to each used tile's hover title, under the existing name/hex/nearest-deltaE line. Node tests and a #usagetest gate cover it.
Diffstat (limited to 'scripts/theme-studio/theme-studio.html')
-rw-r--r--scripts/theme-studio/theme-studio.html47
1 files changed, 44 insertions, 3 deletions
diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html
index 00ddb8adc..6cbe85f5e 100644
--- a/scripts/theme-studio/theme-studio.html
+++ b/scripts/theme-studio/theme-studio.html
@@ -759,6 +759,23 @@ function usedPaletteHexes(palette,syntax,uimap,pkgmap,ground){
for(const app in (pkgmap||{}))for(const face in pkgmap[app])addFace(pkgmap[app][face]);
return used;
}
+// Enumerate where a palette color is used, as "area > element" strings. scopes
+// is [{area, faces:{element: faceObj}}] -- one scope per view area (color/code,
+// ui faces, each package app), element keyed by its display label. A face counts
+// if any of fg / bg / box-color resolves (by hex or palette name) to the target.
+function paletteUsages(hex,scopes,palette){
+ const target=(hex||'').toLowerCase();
+ if(!target)return [];
+ const out=[];
+ for(const {area,faces} of (scopes||[])){
+ for(const element in (faces||{})){
+ const f=faces[element];if(!f)continue;
+ const vals=[f.fg,f.bg,f.box&&f.box.color];
+ if(vals.some(v=>{const h=nameToHex(v,palette);return h&&h.toLowerCase()===target;}))out.push(area+' > '+element);
+ }
+ }
+ return out;
+}
function columnsFromPalette(palette,ground){
const bg=ground&&ground.bg,fg=ground&&ground.fg;
const groundStrip=[];
@@ -1567,7 +1584,7 @@ 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,used){
+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 locked=(role==='bg'||role==='fg');
@@ -1575,6 +1592,7 @@ function paletteChip(i,nearest,used){
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';}
+ else if(scopes){const u=paletteUsages(hex,scopes,PALETTE);if(u.length)d.title+='\n'+u.join('\n');}
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}" readonly style="color:${tc}"><div class="hx" style="color:${tc}">${hex}</div>`;
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});};
@@ -1665,6 +1683,13 @@ function renderPalette(){
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']});
+ // 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=[
+ {area:'color/code assignments',faces:Object.fromEntries(CATS.filter(c=>c[0]!=='bg'&&c[0]!=='p').map(c=>[c[1]||c[0],syntaxFace(c[0])]))},
+ {area:'ui faces',faces:Object.fromEntries(UI_FACES.map(u=>[u[1]||u[0],UIMAP[u[0]]]))},
+ ...Object.keys(APPS).map(app=>({area:APPS[app].label,faces:Object.fromEntries(APPS[app].faces.map(r=>[r[1]||r[0],PKGMAP[app][r[0]]]))}))
+ ];
const used=new Set();
const idxOf=(hex,name)=>{for(let i=0;i<PALETTE.length;i++)if(!used.has(i)&&PALETTE[i][0]===hex&&PALETTE[i][1]===name){used.add(i);return i;}return -1;};
const strip=(cls)=>{const s=document.createElement('div');s.className='fstrip'+(cls||'');p.appendChild(s);return s;};
@@ -1674,7 +1699,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,usedHexes));
+ if(i>=0)gs.appendChild(paletteChip(i,nearest,usedHexes,usageScopes));
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=`<input class="nm" value="${m.name||'ground'}" disabled style="color:${tc}"><div class="hx" style="color:${tc}">${m.hex}</div>`;gs.appendChild(sw);}
});
@@ -1686,7 +1711,7 @@ 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,usedHexes));});
+ (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,usageScopes));});
if(f.members.every(m=>!usedHexes.has(m.hex.toLowerCase())))s.classList.add('unused-col');
});
renderPaletteWarnings(warnings,overflow);
@@ -3317,4 +3342,20 @@ if(location.hash==='#gonetest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c
PALETTE=saveP;for(const k in MAP)delete MAP[k];Object.assign(MAP,saveM);for(const f in UIMAP)delete UIMAP[f];Object.assign(UIMAP,saveU);syncSyntaxFromCache();buildUITable();
document.title='GONETEST '+(ok?'PASS':'FAIL');
const d=document.createElement('div');d.id='gonetest';d.textContent='GONETEST '+(ok?'PASS':'FAIL')+(notes.length?' fails='+notes.join(','):'');document.body.appendChild(d);}
+// Tile-usage-hover gate (open with #usagetest): a tile's title lists the
+// "view area > element" pairings that use its color, under the name/hex line.
+if(location.hash==='#usagetest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}};
+ const saveP=PALETTE.slice(),saveM=Object.assign({},MAP),saveU=JSON.parse(JSON.stringify(UIMAP));
+ setSyntaxFg('bg','#101010');setSyntaxFg('p','#f0f0f0');
+ PALETTE=[['#101010','bg','ground'],['#f0f0f0','fg','ground'],['#67809c','blue','blue']];
+ const f0=UI_FACES[0][0],f0label=UI_FACES[0][1]||f0;
+ for(const f in UIMAP)UIMAP[f]={fg:null,bg:null,bold:false,italic:false,underline:false,strike:false};
+ UIMAP[f0]={fg:null,bg:'#67809c',bold:false,italic:false,underline:false,strike:false};
+ renderPalette();
+ const blueChip=document.querySelector('#pals .fstrip[data-column="blue"] .pchip');
+ A(blueChip&&blueChip.title.includes('ui faces > '+f0label),'hover-title-lists-ui-face-usage');
+ A(blueChip&&blueChip.title.split('\n').length>1,'usage-list-on-its-own-line-under-current-info');
+ PALETTE=saveP;for(const k in MAP)delete MAP[k];Object.assign(MAP,saveM);for(const f in UIMAP)delete UIMAP[f];Object.assign(UIMAP,saveU);syncSyntaxFromCache();renderPalette();
+ document.title='USAGETEST '+(ok?'PASS':'FAIL');
+ const d=document.createElement('div');d.id='usagetest';d.textContent='USAGETEST '+(ok?'PASS':'FAIL')+(notes.length?' fails='+notes.join(','):'');document.body.appendChild(d);}
</script>