diff options
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/theme-studio/app-core.js | 12 | ||||
| -rw-r--r-- | scripts/theme-studio/app.js | 6 | ||||
| -rw-r--r-- | scripts/theme-studio/browser-gates.js | 6 | ||||
| -rw-r--r-- | scripts/theme-studio/test-app-core.mjs | 25 | ||||
| -rw-r--r-- | scripts/theme-studio/theme-studio.html | 22 |
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'); |
