aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio/palette-actions.js
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/theme-studio/palette-actions.js')
-rw-r--r--scripts/theme-studio/palette-actions.js100
1 files changed, 57 insertions, 43 deletions
diff --git a/scripts/theme-studio/palette-actions.js b/scripts/theme-studio/palette-actions.js
index e9b68f67b..6a464ea57 100644
--- a/scripts/theme-studio/palette-actions.js
+++ b/scripts/theme-studio/palette-actions.js
@@ -1,9 +1,9 @@
function clearPalette(){
normalizePalette();
- const plan=clearPalettePlan(PALETTE,{bg:MAP['bg'],fg:MAP['p']});
+ const plan=clearPalettePlan(PALETTE,groundPair());
plan.removed.forEach(({hex,name})=>rememberGone(hex,name));
PALETTE=plan.palette;selectedIdx=null;
- renderPalette();buildTable();buildUITable();if(document.getElementById('pkgbody'))buildPkgTable();renderCode();applyGround();
+ refreshPaletteState();
notify('cleared palette to bg and fg',false);
}
let selectedIdx=null;
@@ -11,14 +11,19 @@ let selectedIdx=null;
// recreating a color with the same name can re-bind the assignments still pointing
// at the old (now "(gone)") hex. Consumed once per name; cleared on import.
let lastGone={};
-// Re-point every assignment — syntax map, UI faces, package faces — from one hex
+// Re-point every assignment — syntax faces, UI faces, package faces — from one hex
// to another. Used when a palette color's value is edited and when a deleted name
// is recreated.
+function repointFaceFields(face,oldHex,newHex){
+ if(face.fg===oldHex)face.fg=newHex;
+ if(face.bg===oldHex)face.bg=newHex;
+ if(face.box&&face.box.color===oldHex)face.box.color=newHex;
+}
function repointHex(oldHex,newHex){
if(oldHex===newHex)return;
- for(const k in MAP){if(MAP[k]===oldHex)MAP[k]=newHex;}
- for(const f in UIMAP){if(UIMAP[f].fg===oldHex)UIMAP[f].fg=newHex;if(UIMAP[f].bg===oldHex)UIMAP[f].bg=newHex;}
- for(const ap in PKGMAP)for(const fc in PKGMAP[ap]){const o=PKGMAP[ap][fc];if(o.fg===oldHex)o.fg=newHex;if(o.bg===oldHex)o.bg=newHex;}
+ for(const k in SYNTAX){repointFaceFields(SYNTAX[k],oldHex,newHex);syncSyntaxCache(k);}
+ for(const f in UIMAP)repointFaceFields(UIMAP[f],oldHex,newHex);
+ for(const ap in PKGMAP)for(const fc in PKGMAP[ap])repointFaceFields(PKGMAP[ap][fc],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.
@@ -29,17 +34,17 @@ function normalizePaletteEntry(entry){
return [hex,name,(entry&&entry[2])||columnIdOf(entry)];
}
function ensureGroundEndpoints(){
- const ground={bg:MAP['bg'],fg:MAP['p']};
- if(ground.bg&&!PALETTE.some(entry=>groundRoleOfEntry(entry,ground)==='bg'))PALETTE.unshift([ground.bg,'bg','ground']);
- if(ground.fg&&!PALETTE.some(entry=>groundRoleOfEntry(entry,ground)==='fg'))PALETTE.push([ground.fg,'fg','ground']);
+ const g=groundPair();
+ if(g.bg&&!PALETTE.some(entry=>groundRoleOfEntry(entry,g)==='bg'))PALETTE.unshift([g.bg,'bg','ground']);
+ if(g.fg&&!PALETTE.some(entry=>groundRoleOfEntry(entry,g)==='fg'))PALETTE.push([g.fg,'fg','ground']);
}
function normalizePalette(){PALETTE=PALETTE.map(normalizePaletteEntry);ensureGroundEndpoints();}
// The ground column is explicit: bg pins the top endpoint, fg pins the bottom
// endpoint, and generated ground+N steps live between them.
function groundColumnMembers(){
- return groundColumnMembersFromPalette(PALETTE,{bg:MAP['bg'],fg:MAP['p']});
+ return groundColumnMembersFromPalette(PALETTE,groundPair());
}
-function groundSpanCount(){return PALETTE.filter(entry=>groundRoleOfEntry(entry,{bg:MAP['bg'],fg:MAP['p']})==='step').length;}
+function groundSpanCount(){return PALETTE.filter(entry=>groundRoleOfEntry(entry,groundPair())==='step').length;}
function groundSpanControl(){
const d=document.createElement('div');d.className='fcount';
d.innerHTML=`<span title="number of ground colors between bg and fg">span <input type="number" min="0" max="8" value="${groundSpanCount()}"></span>`;
@@ -47,7 +52,7 @@ function groundSpanControl(){
return d;
}
function setGroundSpan(n){
- const old=PALETTE.filter(entry=>groundRoleOfEntry(entry,{bg:MAP['bg'],fg:MAP['p']})==='step');
+ const old=PALETTE.filter(entry=>groundRoleOfEntry(entry,groundPair())==='step');
const bg=srgb2oklab(MAP['bg']),fg=srgb2oklab(MAP['p']);
const entries=[];
let step=1;
@@ -62,36 +67,29 @@ function setGroundSpan(n){
const next=entries.find(([,name])=>name===oldName);
if(next&&next[0].toLowerCase()!==oldHex.toLowerCase())repointHex(oldHex,next[0]);
}
- for(let i=PALETTE.length-1;i>=0;i--)if(groundRoleOfEntry(PALETTE[i],{bg:MAP['bg'],fg:MAP['p']})==='step')PALETTE.splice(i,1);
- let at=PALETTE.findIndex(entry=>groundRoleOfEntry(entry,{bg:MAP['bg'],fg:MAP['p']})==='bg');
+ for(let i=PALETTE.length-1;i>=0;i--)if(groundRoleOfEntry(PALETTE[i],groundPair())==='step')PALETTE.splice(i,1);
+ let at=PALETTE.findIndex(entry=>groundRoleOfEntry(entry,groundPair())==='bg');
if(at<0)at=0; else at+=1;
PALETTE.splice(Math.min(at,PALETTE.length),0,...entries);
- selectedIdx=null;renderPalette();buildTable();buildUITable();renderCode();applyGround();
+ selectedIdx=null;refreshPaletteState();
notify('set ground span to '+n,false);
}
// Pairwise OKLab ΔE over the palette. Returns the sub-threshold pairs (sorted
// closest-first) and each color's nearest-neighbor distance for its chip title.
-// Pure pairwise ΔE analysis lives in colormath.js (paletteWarnings); this renders it.
-function renderPaletteWarnings(warnings,overflow){
- const w=document.getElementById('palwarn');if(!w)return;
- if(!warnings.length){w.style.display='none';w.innerHTML='';return;}
- let html='<div class="pwh">too-similar colors</div>';
- html+=warnings.map(p=>`<div class="pwl">${esc(p.aName+' / '+p.bName)} — \u0394E ${p.dE.toFixed(3)}, hard to distinguish</div>`).join('');
- if(overflow>0)html+=`<div class="pwl">and ${overflow} more</div>`;
- w.innerHTML=html;w.style.display='block';
-}
// 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,scopes){
const [hex,name]=PALETTE[i],tc=textOn(hex),nde=nearest[i];
- const role=groundRoleOfEntry(PALETTE[i],{bg:MAP['bg'],fg:MAP['p']});
+ const role=groundRoleOfEntry(PALETTE[i],groundPair());
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';}
+ 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;renderPalette();buildTable();buildUITable();};
+ 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});};
const nm=d.querySelector('.nm');
const finishNameEdit=()=>{nm.readOnly=true;nm.classList.remove('editing');};
nm.onclick=(e)=>{e.preventDefault();e.stopPropagation();selectColor(i);};
@@ -119,11 +117,11 @@ function selectColumnBase(f){
if(i>=0)selectColor(i);
}
function isGroundEntry(entry){
- return !!groundRoleOfEntry(entry,{bg:MAP['bg'],fg:MAP['p']});
+ return !!groundRoleOfEntry(entry,groundPair());
}
function moveColumn(columnId,dir){
normalizePalette();
- const columns=sortColumns(columnsFromPalette(PALETTE,{bg:MAP['bg'],fg:MAP['p']}).columns);
+ const columns=sortColumns(columnsFromPalette(PALETTE,groundPair()).columns);
const pos=columns.findIndex(f=>f.column===columnId);
const next=columns[pos+dir];
if(pos<0||!next)return;
@@ -137,18 +135,18 @@ function moveColumn(columnId,dir){
if(!nextPositions.length)return;
const at=dir<0?nextPositions[0]:nextPositions[nextPositions.length-1]+1;
PALETTE=rest.slice(0,at).concat(moving,rest.slice(at));
- selectedIdx=null;renderPalette();buildTable();buildUITable();renderCode();applyGround();
+ selectedIdx=null;refreshPaletteState();
notify('moved "'+columnId+'" '+(dir<0?'left':'right'),false);
}
function deleteColumn(columnId,label){
normalizePalette();
- const plan=deletePaletteColumnPlan(PALETTE,{bg:MAP['bg'],fg:MAP['p']},columnId);
+ const plan=deletePaletteColumnPlan(PALETTE,groundPair(),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();
+ refreshPaletteState();
notify('deleted column "'+title+'" — '+plan.removed.length+' color(s) now show "(gone)" where used',false);
}
function columnHeader(f,position,count){
@@ -165,11 +163,29 @@ function columnHeader(f,position,count){
// Render the palette as structural color columns: pinned ground column, then
// first-seen palette columns. Grouping uses the stable column id stored on each
// palette entry, so renaming a color never moves it.
+// Palette display mode: full (every span tile) or base-only (one tile per
+// column), toggled by the arrow control to conserve vertical space.
+let paletteShowFull=true;
function renderPalette(){
normalizePalette();
const p=document.getElementById('pals');p.innerHTML='';
- const {warnings,overflow,nearest}=paletteWarnings(PALETTE,DELTAE_MIN,5);
- const {ground,columns}=columnsFromPalette(PALETTE,{bg:MAP['bg'],fg:MAP['p']});
+ const tg=document.createElement('button');tg.className='paltoggle';tg.id='paltoggle';
+ tg.textContent=paletteShowFull?'▼':'▶';
+ tg.title=paletteShowFull?'showing full palette with spans — click for base colors only':'showing base colors only — click for the full palette';
+ tg.onclick=()=>{paletteShowFull=!paletteShowFull;renderPalette();};
+ p.appendChild(tg);
+ // nearest drives the per-chip ΔE tooltip; the too-similar warning box was
+ // removed (the same info is reachable inline via the contrast field).
+ const {nearest}=paletteWarnings(PALETTE,DELTAE_MIN,5);
+ const {ground,columns}=columnsFromPalette(PALETTE,groundPair());
+ const usedHexes=usedPaletteHexes(PALETTE,SYNTAX,UIMAP,PKGMAP,groundPair());
+ // 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;};
@@ -177,23 +193,21 @@ function renderPalette(){
const gs=strip(' ground');gs.dataset.column='ground';
const gh=document.createElement('div');gh.className='fhead';gh.textContent='ground';gs.appendChild(gh);
gs.appendChild(groundSpanControl());
- groundColumnMembers().forEach(m=>{
+ (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,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);}
});
}
- // The too-similar warning stays on the full flat palette, so large spans can
- // still expose genuinely hard-to-distinguish neighboring colors.
const ordered=sortColumns(columns);
ordered.forEach((f,pos)=>{
const s=strip('');s.dataset.column=f.column||f.base;
s.appendChild(columnHeader(f,pos,ordered.length));
s.appendChild(columnCountControl(f));
- f.members.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,usageScopes));});
+ if(f.members.every(m=>!usedHexes.has(m.hex.toLowerCase())))s.classList.add('unused-col');
});
- renderPaletteWarnings(warnings,overflow);
buildUITable();if(document.getElementById('pkgbody'))buildPkgTable();
}
// The per-column count control under a chromatic strip. Its value is the column's
@@ -213,7 +227,7 @@ function columnCountControl(f){
// references and leaving removed ones on their now-gone hex. Returns the removed
// count, or null on a bad base. Shared by the count control and the base edit.
function regenColumnInPlace(oldHexes,baseHex,baseName,n,columnId){
- const r=regenColumn(baseHex,n,{});
+ const r=regenColumn(baseHex,n,{ground:groundPair()});
if(r.error){notify('cannot regenerate from '+baseHex,true);return null;}
const plan=stepRepointPlan(rankByLightness(oldHexes,baseHex),r.members);
const oldSet=new Set(oldHexes.map(h=>h.toLowerCase()));
@@ -227,12 +241,12 @@ function regenColumnInPlace(oldHexes,baseHex,baseName,n,columnId){
return plan.removed.length;
}
function setColumnCount(baseHex,n){
- const {columns}=columnsFromPalette(PALETTE,{bg:MAP['bg'],fg:MAP['p']});
+ const {columns}=columnsFromPalette(PALETTE,groundPair());
const column=columns.find(f=>f.base.toLowerCase()===baseHex.toLowerCase());
if(!column)return;
const baseName=(column.members.find(m=>m.hex.toLowerCase()===baseHex.toLowerCase())||{}).name||'color';
const removed=regenColumnInPlace(column.members.map(m=>m.hex),baseHex,baseName,n,column.column);
if(removed===null)return;
- selectedIdx=null;renderPalette();buildTable();buildUITable();renderCode();applyGround();
+ selectedIdx=null;refreshPaletteState();
notify('regenerated "'+baseName+'" to ±'+n+(removed?(' — '+removed+' removed step(s) show "(gone)" where used'):''),false);
}