aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-16 05:10:38 -0500
committerCraig Jennings <c@cjennings.net>2026-06-16 05:10:38 -0500
commitafd2ddad818cdbf9f4b77d43efb91c35b6c57946 (patch)
tree970b50e89b274aa9d26f46e83d5b77bb55ac2ef4 /scripts
parent1208d96a10e7982650bf1a1118018b5ba471cc60 (diff)
downloaddotemacs-afd2ddad818cdbf9f4b77d43efb91c35b6c57946.tar.gz
dotemacs-afd2ddad818cdbf9f4b77d43efb91c35b6c57946.zip
feat(theme-studio): alphabetize packages in the assignment dropdown
The assignment-view dropdown listed package faces in APPS build order (bespoke apps first, then inventory). generate.py builds them that way, so the list wasn't alphabetical. I added a pure appViewKeysSorted helper that orders the app keys by display label, and buildViewSel uses it. The @code and @ui editor entries above the divider are unchanged.
Diffstat (limited to 'scripts')
-rw-r--r--scripts/theme-studio/app-core.js12
-rw-r--r--scripts/theme-studio/app.js6
-rw-r--r--scripts/theme-studio/browser-gates.js6
-rw-r--r--scripts/theme-studio/test-app-core.mjs25
-rw-r--r--scripts/theme-studio/theme-studio.html22
5 files changed, 57 insertions, 14 deletions
diff --git a/scripts/theme-studio/app-core.js b/scripts/theme-studio/app-core.js
index 739d198db..4df6e8a24 100644
--- a/scripts/theme-studio/app-core.js
+++ b/scripts/theme-studio/app-core.js
@@ -407,4 +407,14 @@ function spanNeighborHex(cur,palette,ground,dir){
return null;
}
-export { nameToHex, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, resolveSyntaxFg, resolveUiAttr, dropdownRowTextColor, paletteOptionList, galleryModel, spanNeighborHex, slugify, fgSetFor, floor, lMax, COVERED_FACES, columnsFromPalette, usedPaletteHexes, paletteUsages, regenColumn, rankByLightness, stepRepointPlan, sortColumns, sortColumnMembers, groundRoleOfEntry, groundColumnMembersFromPalette, clearPalettePlan, deletePaletteColumnPlan, areAllLocked, lockToggleLabel, toggleLockSet };
+// The package apps for the assignment-view dropdown, keyed and sorted by display
+// label (case-insensitive). generate.py builds APPS as bespoke apps first then
+// inventory apps, so the raw key order isn't alphabetical; this orders the list
+// the reader scans. An app missing a label falls back to its key.
+function appViewKeysSorted(apps){
+ return Object.keys(apps||{}).sort((a,b)=>
+ String((apps[a]&&apps[a].label)||a).localeCompare(
+ String((apps[b]&&apps[b].label)||b), undefined, {sensitivity:'base'}));
+}
+
+export { nameToHex, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, resolveSyntaxFg, resolveUiAttr, dropdownRowTextColor, paletteOptionList, galleryModel, appViewKeysSorted, spanNeighborHex, slugify, fgSetFor, floor, lMax, COVERED_FACES, columnsFromPalette, usedPaletteHexes, paletteUsages, 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 a4e0da9c1..3ebd37587 100644
--- a/scripts/theme-studio/app.js
+++ b/scripts/theme-studio/app.js
@@ -519,14 +519,14 @@ function curApp(){const s=document.getElementById('viewsel');const v=s&&s.value;
function pkgEffFg(app,face,seen){return effResolve(PKGMAP,app,face,'fg',seen);}
function pkgEffBg(app,face,seen){return effResolve(PKGMAP,app,face,'bg',seen);}
// One dropdown drives the whole assignment panel: two editor entries (@code,
-// @ui) then a non-selectable "package faces" optgroup holding every app, in
-// APPS order. onViewChange shows exactly one of the three view blocks.
+// @ui) then a non-selectable "package faces" optgroup holding every app,
+// alphabetically by label. onViewChange shows exactly one of the three view blocks.
function buildViewSel(){const s=document.getElementById('viewsel');if(!s)return;s.innerHTML='';
const mk=(v,t)=>{const o=document.createElement('option');o.value=v;o.textContent=t;return o;};
s.appendChild(mk('@code','color/code assignments'));
s.appendChild(mk('@ui','ui faces'));
const og=document.createElement('optgroup');og.label='package faces';
- for(const app in APPS)og.appendChild(mk(app,APPS[app].label));
+ for(const app of appViewKeysSorted(APPS))og.appendChild(mk(app,APPS[app].label));
s.appendChild(og);}
function onViewChange(){const s=document.getElementById('viewsel');const v=(s&&s.value)||'@code';
const show=(id,on)=>{const e=document.getElementById(id);if(e)e.style.display=on?'':'none';};
diff --git a/scripts/theme-studio/browser-gates.js b/scripts/theme-studio/browser-gates.js
index 25e7352f4..0eae09dcf 100644
--- a/scripts/theme-studio/browser-gates.js
+++ b/scripts/theme-studio/browser-gates.js
@@ -674,8 +674,8 @@ if(location.hash==='#roundtriptest'){let ok=true;const notes=[];const A=(c,n)=>{
const d=document.createElement('div');d.id='roundtriptest';d.textContent='ROUNDTRIPTEST '+(ok?'PASS':'FAIL')+(notes.length?' | '+notes.join(' ; '):'');document.body.appendChild(d);}
// View-selector gate (open with #viewtest): the assignment panel is driven by a
// single #viewsel dropdown -- two editor entries (@code, @ui) then a "package
-// faces" optgroup of every app, in order -- and switching it shows exactly one
-// of the three view blocks.
+// faces" optgroup of every app, alphabetically by label -- and switching it
+// shows exactly one of the three view blocks.
if(location.hash==='#viewtest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}};
const sel=document.getElementById('viewsel');
A(!!sel,'viewsel-exists');
@@ -685,7 +685,7 @@ if(location.hash==='#viewtest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c
const og=sel.querySelector('optgroup');
A(og&&og.label==='package faces','package-faces-optgroup');
if(og){const appOpts=[...og.querySelectorAll('option')].map(o=>o.value);
- A(JSON.stringify(appOpts)===JSON.stringify(Object.keys(APPS)),'optgroup-lists-apps-in-order');}
+ A(JSON.stringify(appOpts)===JSON.stringify(appViewKeysSorted(APPS)),'optgroup-lists-apps-alphabetically');}
const vis=id=>{const e=document.getElementById(id);return !!e&&e.style.display!=='none';};
sel.value='@code';onViewChange();
A(vis('view-code')&&!vis('view-ui')&&!vis('view-pkg'),'code-view-only');
diff --git a/scripts/theme-studio/test-app-core.mjs b/scripts/theme-studio/test-app-core.mjs
index 7f537d128..a55abadd0 100644
--- a/scripts/theme-studio/test-app-core.mjs
+++ b/scripts/theme-studio/test-app-core.mjs
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url';
import {
nameToHex, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, resolveSyntaxFg, resolveUiAttr, dropdownRowTextColor, paletteOptionList, spanNeighborHex, slugify,
clearPalettePlan, deletePaletteColumnPlan, groundColumnMembersFromPalette, areAllLocked, lockToggleLabel, toggleLockSet,
- galleryModel,
+ galleryModel, appViewKeysSorted,
} from './app-core.js';
import { planPaletteGenerator, entriesForGeneratedColumn } from './palette-generator-core.js';
import { oklch2hex, deltaE } from './colormath.js';
@@ -823,3 +823,26 @@ test('dropdownRowTextColor: the filled default row contrasts against its fill',
test('dropdownRowTextColor: a default row with no fill inherits (empty)', () => {
assert.equal(dropdownRowTextColor('', '', stubTextOn), '');
});
+
+// appViewKeysSorted: the assignment-view dropdown lists package apps
+// alphabetically by display label, independent of the APPS build order
+// (generate.py emits bespoke apps first, then inventory apps).
+test('appViewKeysSorted: sorts app keys by display label, case-insensitive', () => {
+ const apps = { dashboard: { label: 'Dashboard' }, magit: { label: 'Magit' },
+ alert: { label: 'alert' }, 'web-mode': { label: 'web-mode' } };
+ assert.deepEqual(appViewKeysSorted(apps), ['alert', 'dashboard', 'magit', 'web-mode']);
+});
+test('appViewKeysSorted: bespoke-then-inventory build order comes out alphabetical', () => {
+ const apps = { magit: { label: 'Magit' }, dashboard: { label: 'Dashboard' },
+ alert: { label: 'alert' }, consult: { label: 'consult' } };
+ assert.deepEqual(appViewKeysSorted(apps), ['alert', 'consult', 'dashboard', 'magit']);
+});
+test('appViewKeysSorted: empty or nullish input yields an empty list', () => {
+ assert.deepEqual(appViewKeysSorted({}), []);
+ assert.deepEqual(appViewKeysSorted(null), []);
+ assert.deepEqual(appViewKeysSorted(undefined), []);
+});
+test('appViewKeysSorted: an app with no label falls back to its key for ordering', () => {
+ const apps = { zebra: {}, apple: { label: 'apple' } };
+ assert.deepEqual(appViewKeysSorted(apps), ['apple', 'zebra']);
+});
diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html
index deaba24f2..5501135b5 100644
--- a/scripts/theme-studio/theme-studio.html
+++ b/scripts/theme-studio/theme-studio.html
@@ -911,6 +911,16 @@ function spanNeighborHex(cur,palette,ground,dir){
}
return null;
}
+
+// The package apps for the assignment-view dropdown, keyed and sorted by display
+// label (case-insensitive). generate.py builds APPS as bespoke apps first then
+// inventory apps, so the raw key order isn't alphabetical; this orders the list
+// the reader scans. An app missing a label falls back to its key.
+function appViewKeysSorted(apps){
+ return Object.keys(apps||{}).sort((a,b)=>
+ String((apps[a]&&apps[a].label)||a).localeCompare(
+ String((apps[b]&&apps[b].label)||b), undefined, {sensitivity:'base'}));
+}
// 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
@@ -2083,14 +2093,14 @@ function curApp(){const s=document.getElementById('viewsel');const v=s&&s.value;
function pkgEffFg(app,face,seen){return effResolve(PKGMAP,app,face,'fg',seen);}
function pkgEffBg(app,face,seen){return effResolve(PKGMAP,app,face,'bg',seen);}
// One dropdown drives the whole assignment panel: two editor entries (@code,
-// @ui) then a non-selectable "package faces" optgroup holding every app, in
-// APPS order. onViewChange shows exactly one of the three view blocks.
+// @ui) then a non-selectable "package faces" optgroup holding every app,
+// alphabetically by label. onViewChange shows exactly one of the three view blocks.
function buildViewSel(){const s=document.getElementById('viewsel');if(!s)return;s.innerHTML='';
const mk=(v,t)=>{const o=document.createElement('option');o.value=v;o.textContent=t;return o;};
s.appendChild(mk('@code','color/code assignments'));
s.appendChild(mk('@ui','ui faces'));
const og=document.createElement('optgroup');og.label='package faces';
- for(const app in APPS)og.appendChild(mk(app,APPS[app].label));
+ for(const app of appViewKeysSorted(APPS))og.appendChild(mk(app,APPS[app].label));
s.appendChild(og);}
function onViewChange(){const s=document.getElementById('viewsel');const v=(s&&s.value)||'@code';
const show=(id,on)=>{const e=document.getElementById(id);if(e)e.style.display=on?'':'none';};
@@ -3283,8 +3293,8 @@ if(location.hash==='#roundtriptest'){let ok=true;const notes=[];const A=(c,n)=>{
const d=document.createElement('div');d.id='roundtriptest';d.textContent='ROUNDTRIPTEST '+(ok?'PASS':'FAIL')+(notes.length?' | '+notes.join(' ; '):'');document.body.appendChild(d);}
// View-selector gate (open with #viewtest): the assignment panel is driven by a
// single #viewsel dropdown -- two editor entries (@code, @ui) then a "package
-// faces" optgroup of every app, in order -- and switching it shows exactly one
-// of the three view blocks.
+// faces" optgroup of every app, alphabetically by label -- and switching it
+// shows exactly one of the three view blocks.
if(location.hash==='#viewtest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c){ok=false;notes.push(n);}};
const sel=document.getElementById('viewsel');
A(!!sel,'viewsel-exists');
@@ -3294,7 +3304,7 @@ if(location.hash==='#viewtest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c
const og=sel.querySelector('optgroup');
A(og&&og.label==='package faces','package-faces-optgroup');
if(og){const appOpts=[...og.querySelectorAll('option')].map(o=>o.value);
- A(JSON.stringify(appOpts)===JSON.stringify(Object.keys(APPS)),'optgroup-lists-apps-in-order');}
+ A(JSON.stringify(appOpts)===JSON.stringify(appViewKeysSorted(APPS)),'optgroup-lists-apps-alphabetically');}
const vis=id=>{const e=document.getElementById(id);return !!e&&e.style.display!=='none';};
sel.value='@code';onViewChange();
A(vis('view-code')&&!vis('view-ui')&&!vis('view-pkg'),'code-view-only');