aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-13 19:04:17 -0500
committerCraig Jennings <c@cjennings.net>2026-06-13 19:04:17 -0500
commit009cbf08da23ac5ac7ea00ba0cc49db07e589d83 (patch)
treef69cc3c5d9955abf15888521b44e3b531739ef56 /scripts
parent0958a3d92e1059e2d86a73edb9ba990260c8dcd0 (diff)
downloaddotemacs-009cbf08da23ac5ac7ea00ba0cc49db07e589d83.tar.gz
dotemacs-009cbf08da23ac5ac7ea00ba0cc49db07e589d83.zip
Add theme studio face color step arrows
Diffstat (limited to 'scripts')
-rw-r--r--scripts/theme-studio/README.md4
-rw-r--r--scripts/theme-studio/app-core.js20
-rw-r--r--scripts/theme-studio/app.js23
-rw-r--r--scripts/theme-studio/browser-gates.js26
-rw-r--r--scripts/theme-studio/styles.css8
-rw-r--r--scripts/theme-studio/test-app-core.mjs32
-rw-r--r--scripts/theme-studio/theme-studio.html75
7 files changed, 158 insertions, 30 deletions
diff --git a/scripts/theme-studio/README.md b/scripts/theme-studio/README.md
index 2b4acc6c..e319a324 100644
--- a/scripts/theme-studio/README.md
+++ b/scripts/theme-studio/README.md
@@ -152,6 +152,10 @@ derived from hue, chroma, lightness, or the visible color name.
- **Dropdown order.** Color dropdowns show the default entry, then `bg` and `fg`,
then palette columns from left to right. Within each column's dropdown group,
colors are ordered lightest to darkest.
+- **Dropdown arrows.** Color dropdowns in the syntax, UI, and package face tables
+ have left/right arrows. Left steps to the next darker color in the selected
+ color's column; right steps to the next lighter color. The arrows are disabled
+ for defaults, gone colors, locked rows, and column ends.
The standalone ramp generator is gone; fanning a color into a ramp is now "add the
color, then raise its column's count."
diff --git a/scripts/theme-studio/app-core.js b/scripts/theme-studio/app-core.js
index 4f1eee16..af90f13a 100644
--- a/scripts/theme-studio/app-core.js
+++ b/scripts/theme-studio/app-core.js
@@ -303,5 +303,23 @@ function paletteOptionList(cur,palette,ground){
sortColumns(grouped.columns).forEach(f=>lightestFirstMembers(f.members).forEach(m=>add(m.hex,m.name)));
return out;
}
+function spanNeighborHex(cur,palette,ground,dir){
+ if(!cur)return null;
+ const wanted=(cur||'').toLowerCase(),groups=[],byLight=(a,b)=>oklchOf(a.hex).L-oklchOf(b.hex).L;
+ const addGroup=members=>{
+ const seen=new Set(),g=[];
+ members.filter(m=>m&&m.hex).sort(byLight).forEach(m=>{const h=m.hex.toLowerCase();if(!seen.has(h)){seen.add(h);g.push(m);}});
+ if(g.length)groups.push(g);
+ };
+ addGroup(groundColumnMembersFromPalette(palette,ground||{}));
+ sortColumns(columnsFromPalette(palette,ground||{}).columns).forEach(f=>addGroup(f.members));
+ for(const g of groups){
+ const i=g.findIndex(m=>(m.hex||'').toLowerCase()===wanted);
+ if(i<0)continue;
+ const next=g[i+(dir>0?1:-1)];
+ return next?next.hex:null;
+ }
+ return null;
+}
-export { nameToHex, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, optList, paletteOptionList, slugify, ramp, fgSetFor, floor, lMax, COVERED_FACES, columnsFromPalette, regenColumn, rankByLightness, stepRepointPlan, sortColumns, sortColumnMembers, groundRoleOfEntry, groundColumnMembersFromPalette, clearPalettePlan, deletePaletteColumnPlan, areAllLocked, lockToggleLabel, toggleLockSet };
+export { nameToHex, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, optList, paletteOptionList, spanNeighborHex, slugify, ramp, fgSetFor, floor, lMax, COVERED_FACES, columnsFromPalette, regenColumn, rankByLightness, stepRepointPlan, sortColumns, sortColumnMembers, groundRoleOfEntry, groundColumnMembersFromPalette, clearPalettePlan, deletePaletteColumnPlan, areAllLocked, lockToggleLabel, toggleLockSet };
diff --git a/scripts/theme-studio/app.js b/scripts/theme-studio/app.js
index 14c94f4b..88ddbaa9 100644
--- a/scripts/theme-studio/app.js
+++ b/scripts/theme-studio/app.js
@@ -47,12 +47,24 @@ let _ddPop=null;
function closeColorDropdown(){if(_ddPop){_ddPop.remove();_ddPop=null;}}
document.addEventListener('pointerdown',e=>{if(_ddPop&&!e.target.closest('.cdd')&&!e.target.closest('.cddpop'))closeColorDropdown();});
function mkColorDropdown(options,cur,onPick){
+ const wrap=document.createElement('div');wrap.className='cstep';
+ const left=document.createElement('button'),right=document.createElement('button');
+ left.className='cstepbtn';right.className='cstepbtn';left.type=right.type='button';
+ left.textContent='‹';right.textContent='›';left.title='move to next darker color in this column';right.title='move to next lighter color in this column';
const t=document.createElement('div');t.className='cdd';t.tabIndex=0;
const nameOf=h=>{const o=options.find(p=>p[0]===h);return o?o[1]:(h||'none');};
+ function step(dir){if(wrap.dataset.locked==='1')return;const next=spanNeighborHex(cur,PALETTE,{bg:MAP['bg'],fg:MAP['p']},dir);if(!next)return;cur=next;paint();onPick(next);}
+ function paintStepButtons(){
+ const locked=wrap.dataset.locked==='1';
+ left.disabled=locked||!spanNeighborHex(cur,PALETTE,{bg:MAP['bg'],fg:MAP['p']},-1);
+ right.disabled=locked||!spanNeighborHex(cur,PALETTE,{bg:MAP['bg'],fg:MAP['p']},1);
+ }
function paint(){t.style.background=cur||'#161412';t.style.color=cur?textOn(cur):'#b4b1a2';t.dataset.val=cur||'';
- t.innerHTML=`<span class="cddsw" style="background:${cur||'transparent'}"></span>${esc(nameOf(cur))}`;}
+ t.innerHTML=`<span class="cddsw" style="background:${cur||'transparent'}"></span>${esc(nameOf(cur))}`;paintStepButtons();}
paint();
- t.onclick=(e)=>{e.stopPropagation();if(t.dataset.locked==='1')return;if(_ddPop){closeColorDropdown();return;}
+ left.onclick=e=>{e.stopPropagation();step(-1);};
+ right.onclick=e=>{e.stopPropagation();step(1);};
+ t.onclick=(e)=>{e.stopPropagation();if(wrap.dataset.locked==='1')return;if(_ddPop){closeColorDropdown();return;}
const pop=document.createElement('div');pop.className='cddpop';
for(const [hex,name] of options){const row=document.createElement('div');row.className='cddrow'+(hex===cur?' sel':'');
row.innerHTML=`<span class="cddsw" style="background:${hex||'transparent'}"></span><span class="cddnm">${esc(name)}</span><span class="cddhx">${hex||''}</span>`;
@@ -65,7 +77,10 @@ function mkColorDropdown(options,cur,onPick){
if(r.bottom+ph>window.innerHeight-6)pop.style.top=Math.max(6,r.top-ph-2)+'px';
_ddPop=pop;};
t.setValue=h=>{cur=h;paint();};
- return t;}
+ wrap.setValue=h=>{cur=h;paint();};
+ wrap.syncLocked=paintStepButtons;
+ wrap.appendChild(left);wrap.appendChild(t);wrap.appendChild(right);paintStepButtons();
+ return wrap;}
// Standard option list for a swatch dropdown: a "default" entry, then the
// palette in the same ground/column order as the palette panel. If cur is set
// but no longer in the palette, surface it as a "(gone)" entry so the row still
@@ -82,7 +97,7 @@ function mkLockCell(lockKey,els){
lk.title=on?'locked — click to unlock':'click to lock this decision';
(els||[]).forEach(el=>{if(!el)return;
if(el.tagName==='SELECT'||el.tagName==='BUTTON'||el.tagName==='INPUT')el.disabled=on;
- else{el.dataset.locked=on?'1':'';el.classList.toggle('locked',on);}});}
+ else{el.dataset.locked=on?'1':'';el.classList.toggle('locked',on);if(el.syncLocked)el.syncLocked();}});}
lk.onclick=()=>{LOCKED.has(lockKey)?LOCKED.delete(lockKey):LOCKED.add(lockKey);paint();updateLockToggles();};
paint();td.appendChild(lk);return td;}
// B/I/U/S style buttons shared by the UI and package tables. isOn(attr) reads the
diff --git a/scripts/theme-studio/browser-gates.js b/scripts/theme-studio/browser-gates.js
index 1e47b5ee..cf45c317 100644
--- a/scripts/theme-studio/browser-gates.js
+++ b/scripts/theme-studio/browser-gates.js
@@ -28,16 +28,26 @@ if(location.hash==='#selftest')pkgSelftest();
if(location.hash==='#locktest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}};
LOCKED.clear();buildTable();
{const k=CATS.map(c=>c[0]).filter(k=>k!=='bg'&&k!=='p')[0];
- const tr=document.querySelector('#legbody tr[data-kind="'+k+'"]'),dd=tr.querySelector('.cdd'),lb=tr.querySelector('.lockbtn');
- A(dd.dataset.locked!=='1','syntax-dd-starts-unlocked');lb.click();
- A(dd.dataset.locked==='1'&&dd.classList.contains('locked'),'syntax-lock-disables-dd');lb.click();
- A(dd.dataset.locked!=='1','syntax-unlock-reenables-dd');}
+ const tr=document.querySelector('#legbody tr[data-kind="'+k+'"]'),step=tr.querySelector('.cstep'),dd=tr.querySelector('.cdd'),lb=tr.querySelector('.lockbtn');
+ A(step.dataset.locked!=='1','syntax-dd-starts-unlocked');lb.click();
+ A(step.dataset.locked==='1'&&step.classList.contains('locked')&&step.querySelector('.cstepbtn').disabled,'syntax-lock-disables-dd');lb.click();
+ A(step.dataset.locked!=='1'&&!step.classList.contains('locked'),'syntax-unlock-reenables-dd');}
LOCKED.clear();buildUITable();
{const f=UI_FACES[0][0];
- const tr=document.querySelector('#uibody tr[data-face="'+f+'"]'),dd=tr.querySelector('.cdd'),lb=tr.querySelector('.lockbtn');
- A(dd.dataset.locked!=='1','ui-dd-starts-unlocked');lb.click();
- A(dd.dataset.locked==='1'&&dd.classList.contains('locked'),'ui-lock-disables-dd');lb.click();
- A(dd.dataset.locked!=='1','ui-unlock-reenables-dd');}
+ const tr=document.querySelector('#uibody tr[data-face="'+f+'"]'),step=tr.querySelector('.cstep'),dd=tr.querySelector('.cdd'),lb=tr.querySelector('.lockbtn');
+ A(step.dataset.locked!=='1','ui-dd-starts-unlocked');lb.click();
+ A(step.dataset.locked==='1'&&step.classList.contains('locked')&&step.querySelector('.cstepbtn').disabled,'ui-lock-disables-dd');lb.click();
+ A(step.dataset.locked!=='1'&&!step.classList.contains('locked'),'ui-unlock-reenables-dd');}
+ {PALETTE=[['#000000','bg','ground'],['#ffffff','fg','ground'],['#222222','gray-dark','gray'],['#888888','gray-mid','gray'],['#dddddd','gray-light','gray']];MAP['bg']='#000000';MAP['p']='#ffffff';MAP['kw']='#888888';LOCKED.clear();buildTable();
+ const tr=document.querySelector('#legbody tr[data-kind="kw"]'),btns=tr.querySelectorAll('.cstepbtn');btns[1].click();
+ A(MAP['kw']==='#dddddd'&&tr.querySelector('.cdd').dataset.val==='#dddddd','syntax right arrow steps to lighter color');btns[0].click();
+ A(MAP['kw']==='#888888','syntax left arrow steps to darker color');}
+ {UIMAP['region'].bg='#888888';LOCKED.clear();buildUITable();const tr=document.querySelector('#uibody tr[data-face="region"]'),btns=tr.cells[3].querySelectorAll('.cstepbtn');btns[1].click();
+ A(UIMAP['region'].bg==='#dddddd','ui right arrow steps to lighter color');btns[0].click();
+ A(UIMAP['region'].bg==='#888888','ui left arrow steps to darker color');}
+ {const app=curApp(),face=APPS[app].faces[0][0];PKGMAP[app][face].fg='#888888';LOCKED.clear();buildPkgTable();const tr=document.querySelector('#pkgbody tr[data-face="'+face+'"]'),btns=tr.cells[2].querySelectorAll('.cstepbtn');btns[1].click();
+ A(PKGMAP[app][face].fg==='#dddddd','pkg right arrow steps to lighter color');btns[0].click();
+ A(PKGMAP[app][face].fg==='#888888','pkg left arrow steps to darker color');}
{const ks=CATS.map(c=>c[0]).filter(k=>k!=='bg'&&k!=='p'),k1=ks[0],k2=ks[1];
MAP[k1]='#111111';MAP[k2]='#222222';LOCKED.clear();LOCKED.add(k1);clearUnlocked();
A(MAP[k1]==='#111111','syntax-clear-keeps-locked');A(MAP[k2]==='','syntax-clear-wipes-unlocked');}
diff --git a/scripts/theme-studio/styles.css b/scripts/theme-studio/styles.css
index 21c4030e..78feb4ce 100644
--- a/scripts/theme-studio/styles.css
+++ b/scripts/theme-studio/styles.css
@@ -8,7 +8,11 @@
table.leg th{cursor:pointer;color:#b4b1a2;text-align:left;padding:4px 12px;user-select:none;font-weight:normal}
table.leg th:hover{color:#e8bd30}
select.chip{appearance:none;border:1px solid #00000060;border-radius:5px;padding:5px 10px;font:bold 14px monospace;width:160px;cursor:pointer}
- .cdd{display:inline-flex;align-items:center;gap:7px;border:1px solid #00000060;border-radius:5px;padding:5px 10px;font:bold 13px monospace;width:160px;cursor:pointer;box-sizing:border-box;overflow:hidden;white-space:nowrap}
+ .cstep{display:inline-flex;align-items:center;gap:4px}
+ .cstepbtn{width:22px;height:28px;padding:0;border:1px solid #3a3a3a;border-radius:4px;background:#1f1c19;color:#e8bd30;font:bold 14px monospace;cursor:pointer}
+ .cstepbtn:disabled{opacity:.28;cursor:default;color:#8f8977}
+ .cstep.locked .cstepbtn{opacity:.28;cursor:default}
+ .cdd{display:inline-flex;align-items:center;gap:7px;border:1px solid #00000060;border-radius:5px;padding:5px 10px;font:bold 13px monospace;width:150px;cursor:pointer;box-sizing:border-box;overflow:hidden;white-space:nowrap}
.cddsw{display:inline-block;width:13px;height:13px;border-radius:3px;border:1px solid #0007;flex:none}
.cddpop{position:fixed;z-index:200;background:#161412;border:1px solid #3a3a3a;border-radius:6px;box-shadow:0 12px 34px #000c;max-height:60vh;overflow:auto;padding:4px}
.cddrow{display:flex;align-items:center;gap:9px;padding:4px 9px;cursor:pointer;color:#cdced1;font:12px monospace;border-radius:4px;white-space:nowrap}
@@ -16,7 +20,7 @@
.cddrow.sel{outline:1px solid #e8bd30;outline-offset:-1px}
.cddrow .cddnm{flex:1}
.cddrow .cddhx{opacity:.55;margin-left:10px}
- .cdd.locked{cursor:default;opacity:.85;box-shadow:inset 0 0 0 2px #e8bd3088}
+ .cstep.locked .cdd{cursor:default;opacity:.85;box-shadow:inset 0 0 0 2px #e8bd3088}
.lockbtn{background:none;border:none;cursor:pointer;font-size:15px;line-height:1;padding:2px 4px;opacity:.5;filter:grayscale(1)}
.lockbtn.on{opacity:1;filter:none}
.legctl{margin:0 0 8px;display:flex;gap:8px;align-items:center}
diff --git a/scripts/theme-studio/test-app-core.mjs b/scripts/theme-studio/test-app-core.mjs
index 48318305..ded3da94 100644
--- a/scripts/theme-studio/test-app-core.mjs
+++ b/scripts/theme-studio/test-app-core.mjs
@@ -7,7 +7,7 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import {
- nameToHex, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, optList, paletteOptionList, slugify,
+ nameToHex, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, optList, paletteOptionList, spanNeighborHex, slugify,
clearPalettePlan, deletePaletteColumnPlan, groundColumnMembersFromPalette, areAllLocked, lockToggleLabel, toggleLockSet,
} from './app-core.js';
@@ -100,6 +100,36 @@ test('paletteOptionList: Error — a cur outside palette and ground is surfaced
assert.deepEqual(list[1], ['#123456', '(gone) #123456']);
});
+test('spanNeighborHex: Normal — steps lighter and darker within the current column', () => {
+ const pal = [
+ ['#222222', 'gray-dark', 'gray'],
+ ['#888888', 'gray-mid', 'gray'],
+ ['#dddddd', 'gray-light', 'gray'],
+ ['#330000', 'red-dark', 'red'],
+ ];
+ const ground = { bg: '#000000', fg: '#ffffff' };
+ assert.equal(spanNeighborHex('#888888', pal, ground, 1), '#dddddd');
+ assert.equal(spanNeighborHex('#888888', pal, ground, -1), '#222222');
+ assert.equal(spanNeighborHex('#dddddd', pal, ground, 1), null);
+ assert.equal(spanNeighborHex('#222222', pal, ground, -1), null);
+});
+
+test('spanNeighborHex: Normal — ground steps by lightness too', () => {
+ const pal = [
+ ['#ffffff', 'bg', 'ground'],
+ ['#777777', 'ground+1', 'ground'],
+ ['#000000', 'fg', 'ground'],
+ ];
+ const ground = { bg: '#ffffff', fg: '#000000' };
+ assert.equal(spanNeighborHex('#777777', pal, ground, 1), '#ffffff');
+ assert.equal(spanNeighborHex('#777777', pal, ground, -1), '#000000');
+});
+
+test('spanNeighborHex: Boundary — default and gone colors cannot step', () => {
+ assert.equal(spanNeighborHex('', PAL, { bg: '#000000', fg: '#ffffff' }, 1), null);
+ assert.equal(spanNeighborHex('#123456', PAL, { bg: '#000000', fg: '#ffffff' }, 1), null);
+});
+
test('clearPalettePlan: Normal — removes non-ground colors and records recoverable names', () => {
const plan = clearPalettePlan([
['#0d0b0a', 'bg', 'ground'],
diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html
index 51c358b8..ef439993 100644
--- a/scripts/theme-studio/theme-studio.html
+++ b/scripts/theme-studio/theme-studio.html
@@ -10,7 +10,11 @@
table.leg th{cursor:pointer;color:#b4b1a2;text-align:left;padding:4px 12px;user-select:none;font-weight:normal}
table.leg th:hover{color:#e8bd30}
select.chip{appearance:none;border:1px solid #00000060;border-radius:5px;padding:5px 10px;font:bold 14px monospace;width:160px;cursor:pointer}
- .cdd{display:inline-flex;align-items:center;gap:7px;border:1px solid #00000060;border-radius:5px;padding:5px 10px;font:bold 13px monospace;width:160px;cursor:pointer;box-sizing:border-box;overflow:hidden;white-space:nowrap}
+ .cstep{display:inline-flex;align-items:center;gap:4px}
+ .cstepbtn{width:22px;height:28px;padding:0;border:1px solid #3a3a3a;border-radius:4px;background:#1f1c19;color:#e8bd30;font:bold 14px monospace;cursor:pointer}
+ .cstepbtn:disabled{opacity:.28;cursor:default;color:#8f8977}
+ .cstep.locked .cstepbtn{opacity:.28;cursor:default}
+ .cdd{display:inline-flex;align-items:center;gap:7px;border:1px solid #00000060;border-radius:5px;padding:5px 10px;font:bold 13px monospace;width:150px;cursor:pointer;box-sizing:border-box;overflow:hidden;white-space:nowrap}
.cddsw{display:inline-block;width:13px;height:13px;border-radius:3px;border:1px solid #0007;flex:none}
.cddpop{position:fixed;z-index:200;background:#161412;border:1px solid #3a3a3a;border-radius:6px;box-shadow:0 12px 34px #000c;max-height:60vh;overflow:auto;padding:4px}
.cddrow{display:flex;align-items:center;gap:9px;padding:4px 9px;cursor:pointer;color:#cdced1;font:12px monospace;border-radius:4px;white-space:nowrap}
@@ -18,7 +22,7 @@
.cddrow.sel{outline:1px solid #e8bd30;outline-offset:-1px}
.cddrow .cddnm{flex:1}
.cddrow .cddhx{opacity:.55;margin-left:10px}
- .cdd.locked{cursor:default;opacity:.85;box-shadow:inset 0 0 0 2px #e8bd3088}
+ .cstep.locked .cdd{cursor:default;opacity:.85;box-shadow:inset 0 0 0 2px #e8bd3088}
.lockbtn{background:none;border:none;cursor:pointer;font-size:15px;line-height:1;padding:2px 4px;opacity:.5;filter:grayscale(1)}
.lockbtn.on{opacity:1;filter:none}
.legctl{margin:0 0 8px;display:flex;gap:8px;align-items:center}
@@ -723,6 +727,24 @@ function paletteOptionList(cur,palette,ground){
sortColumns(grouped.columns).forEach(f=>lightestFirstMembers(f.members).forEach(m=>add(m.hex,m.name)));
return out;
}
+function spanNeighborHex(cur,palette,ground,dir){
+ if(!cur)return null;
+ const wanted=(cur||'').toLowerCase(),groups=[],byLight=(a,b)=>oklchOf(a.hex).L-oklchOf(b.hex).L;
+ const addGroup=members=>{
+ const seen=new Set(),g=[];
+ members.filter(m=>m&&m.hex).sort(byLight).forEach(m=>{const h=m.hex.toLowerCase();if(!seen.has(h)){seen.add(h);g.push(m);}});
+ if(g.length)groups.push(g);
+ };
+ addGroup(groundColumnMembersFromPalette(palette,ground||{}));
+ sortColumns(columnsFromPalette(palette,ground||{}).columns).forEach(f=>addGroup(f.members));
+ for(const g of groups){
+ const i=g.findIndex(m=>(m.hex||'').toLowerCase()===wanted);
+ if(i<0)continue;
+ const next=g[i+(dir>0?1:-1)];
+ return next?next.hex:null;
+ }
+ return null;
+}
// Pure color/UI-boundary helpers (normHex, ratingColor, textOn), inlined from
// app-util.js. textOn uses rl from the colormath core above.
// Pure color/UI-boundary helpers: hex-input parsing, the contrast-rating status
@@ -770,12 +792,24 @@ let _ddPop=null;
function closeColorDropdown(){if(_ddPop){_ddPop.remove();_ddPop=null;}}
document.addEventListener('pointerdown',e=>{if(_ddPop&&!e.target.closest('.cdd')&&!e.target.closest('.cddpop'))closeColorDropdown();});
function mkColorDropdown(options,cur,onPick){
+ const wrap=document.createElement('div');wrap.className='cstep';
+ const left=document.createElement('button'),right=document.createElement('button');
+ left.className='cstepbtn';right.className='cstepbtn';left.type=right.type='button';
+ left.textContent='‹';right.textContent='›';left.title='move to next darker color in this column';right.title='move to next lighter color in this column';
const t=document.createElement('div');t.className='cdd';t.tabIndex=0;
const nameOf=h=>{const o=options.find(p=>p[0]===h);return o?o[1]:(h||'none');};
+ function step(dir){if(wrap.dataset.locked==='1')return;const next=spanNeighborHex(cur,PALETTE,{bg:MAP['bg'],fg:MAP['p']},dir);if(!next)return;cur=next;paint();onPick(next);}
+ function paintStepButtons(){
+ const locked=wrap.dataset.locked==='1';
+ left.disabled=locked||!spanNeighborHex(cur,PALETTE,{bg:MAP['bg'],fg:MAP['p']},-1);
+ right.disabled=locked||!spanNeighborHex(cur,PALETTE,{bg:MAP['bg'],fg:MAP['p']},1);
+ }
function paint(){t.style.background=cur||'#161412';t.style.color=cur?textOn(cur):'#b4b1a2';t.dataset.val=cur||'';
- t.innerHTML=`<span class="cddsw" style="background:${cur||'transparent'}"></span>${esc(nameOf(cur))}`;}
+ t.innerHTML=`<span class="cddsw" style="background:${cur||'transparent'}"></span>${esc(nameOf(cur))}`;paintStepButtons();}
paint();
- t.onclick=(e)=>{e.stopPropagation();if(t.dataset.locked==='1')return;if(_ddPop){closeColorDropdown();return;}
+ left.onclick=e=>{e.stopPropagation();step(-1);};
+ right.onclick=e=>{e.stopPropagation();step(1);};
+ t.onclick=(e)=>{e.stopPropagation();if(wrap.dataset.locked==='1')return;if(_ddPop){closeColorDropdown();return;}
const pop=document.createElement('div');pop.className='cddpop';
for(const [hex,name] of options){const row=document.createElement('div');row.className='cddrow'+(hex===cur?' sel':'');
row.innerHTML=`<span class="cddsw" style="background:${hex||'transparent'}"></span><span class="cddnm">${esc(name)}</span><span class="cddhx">${hex||''}</span>`;
@@ -788,7 +822,10 @@ function mkColorDropdown(options,cur,onPick){
if(r.bottom+ph>window.innerHeight-6)pop.style.top=Math.max(6,r.top-ph-2)+'px';
_ddPop=pop;};
t.setValue=h=>{cur=h;paint();};
- return t;}
+ wrap.setValue=h=>{cur=h;paint();};
+ wrap.syncLocked=paintStepButtons;
+ wrap.appendChild(left);wrap.appendChild(t);wrap.appendChild(right);paintStepButtons();
+ return wrap;}
// Standard option list for a swatch dropdown: a "default" entry, then the
// palette in the same ground/column order as the palette panel. If cur is set
// but no longer in the palette, surface it as a "(gone)" entry so the row still
@@ -805,7 +842,7 @@ function mkLockCell(lockKey,els){
lk.title=on?'locked — click to unlock':'click to lock this decision';
(els||[]).forEach(el=>{if(!el)return;
if(el.tagName==='SELECT'||el.tagName==='BUTTON'||el.tagName==='INPUT')el.disabled=on;
- else{el.dataset.locked=on?'1':'';el.classList.toggle('locked',on);}});}
+ else{el.dataset.locked=on?'1':'';el.classList.toggle('locked',on);if(el.syncLocked)el.syncLocked();}});}
lk.onclick=()=>{LOCKED.has(lockKey)?LOCKED.delete(lockKey):LOCKED.add(lockKey);paint();updateLockToggles();};
paint();td.appendChild(lk);return td;}
// B/I/U/S style buttons shared by the UI and package tables. isOn(attr) reads the
@@ -1800,16 +1837,26 @@ if(location.hash==='#selftest')pkgSelftest();
if(location.hash==='#locktest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}};
LOCKED.clear();buildTable();
{const k=CATS.map(c=>c[0]).filter(k=>k!=='bg'&&k!=='p')[0];
- const tr=document.querySelector('#legbody tr[data-kind="'+k+'"]'),dd=tr.querySelector('.cdd'),lb=tr.querySelector('.lockbtn');
- A(dd.dataset.locked!=='1','syntax-dd-starts-unlocked');lb.click();
- A(dd.dataset.locked==='1'&&dd.classList.contains('locked'),'syntax-lock-disables-dd');lb.click();
- A(dd.dataset.locked!=='1','syntax-unlock-reenables-dd');}
+ const tr=document.querySelector('#legbody tr[data-kind="'+k+'"]'),step=tr.querySelector('.cstep'),dd=tr.querySelector('.cdd'),lb=tr.querySelector('.lockbtn');
+ A(step.dataset.locked!=='1','syntax-dd-starts-unlocked');lb.click();
+ A(step.dataset.locked==='1'&&step.classList.contains('locked')&&step.querySelector('.cstepbtn').disabled,'syntax-lock-disables-dd');lb.click();
+ A(step.dataset.locked!=='1'&&!step.classList.contains('locked'),'syntax-unlock-reenables-dd');}
LOCKED.clear();buildUITable();
{const f=UI_FACES[0][0];
- const tr=document.querySelector('#uibody tr[data-face="'+f+'"]'),dd=tr.querySelector('.cdd'),lb=tr.querySelector('.lockbtn');
- A(dd.dataset.locked!=='1','ui-dd-starts-unlocked');lb.click();
- A(dd.dataset.locked==='1'&&dd.classList.contains('locked'),'ui-lock-disables-dd');lb.click();
- A(dd.dataset.locked!=='1','ui-unlock-reenables-dd');}
+ const tr=document.querySelector('#uibody tr[data-face="'+f+'"]'),step=tr.querySelector('.cstep'),dd=tr.querySelector('.cdd'),lb=tr.querySelector('.lockbtn');
+ A(step.dataset.locked!=='1','ui-dd-starts-unlocked');lb.click();
+ A(step.dataset.locked==='1'&&step.classList.contains('locked')&&step.querySelector('.cstepbtn').disabled,'ui-lock-disables-dd');lb.click();
+ A(step.dataset.locked!=='1'&&!step.classList.contains('locked'),'ui-unlock-reenables-dd');}
+ {PALETTE=[['#000000','bg','ground'],['#ffffff','fg','ground'],['#222222','gray-dark','gray'],['#888888','gray-mid','gray'],['#dddddd','gray-light','gray']];MAP['bg']='#000000';MAP['p']='#ffffff';MAP['kw']='#888888';LOCKED.clear();buildTable();
+ const tr=document.querySelector('#legbody tr[data-kind="kw"]'),btns=tr.querySelectorAll('.cstepbtn');btns[1].click();
+ A(MAP['kw']==='#dddddd'&&tr.querySelector('.cdd').dataset.val==='#dddddd','syntax right arrow steps to lighter color');btns[0].click();
+ A(MAP['kw']==='#888888','syntax left arrow steps to darker color');}
+ {UIMAP['region'].bg='#888888';LOCKED.clear();buildUITable();const tr=document.querySelector('#uibody tr[data-face="region"]'),btns=tr.cells[3].querySelectorAll('.cstepbtn');btns[1].click();
+ A(UIMAP['region'].bg==='#dddddd','ui right arrow steps to lighter color');btns[0].click();
+ A(UIMAP['region'].bg==='#888888','ui left arrow steps to darker color');}
+ {const app=curApp(),face=APPS[app].faces[0][0];PKGMAP[app][face].fg='#888888';LOCKED.clear();buildPkgTable();const tr=document.querySelector('#pkgbody tr[data-face="'+face+'"]'),btns=tr.cells[2].querySelectorAll('.cstepbtn');btns[1].click();
+ A(PKGMAP[app][face].fg==='#dddddd','pkg right arrow steps to lighter color');btns[0].click();
+ A(PKGMAP[app][face].fg==='#888888','pkg left arrow steps to darker color');}
{const ks=CATS.map(c=>c[0]).filter(k=>k!=='bg'&&k!=='p'),k1=ks[0],k2=ks[1];
MAP[k1]='#111111';MAP[k2]='#222222';LOCKED.clear();LOCKED.add(k1);clearUnlocked();
A(MAP[k1]==='#111111','syntax-clear-keeps-locked');A(MAP[k2]==='','syntax-clear-wipes-unlocked');}