From 3e8d5651a3fddcf4afccd46a382ab12d915bbd8c Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 8 Jun 2026 06:44:56 -0500 Subject: feat(theme-selector): sortable package and UI face tables The package table (face, fg, bg, inherit, size, contrast) and the UI table (face, foreground, background) now sort on a header click, like the code/color assignments table. A generic comparator reads a select value, a numeric input, or cell text (numeric when it leads with a number, so size and contrast sort by value). The sort is remembered per table and re-applied after a rebuild, so editing a face no longer resets the order. --- scripts/theme-selector/generate.py | 14 ++++++++++++-- scripts/theme-selector/theme-selector.html | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/scripts/theme-selector/generate.py b/scripts/theme-selector/generate.py index ed7cddb1..8e76c174 100644 --- a/scripts/theme-selector/generate.py +++ b/scripts/theme-selector/generate.py @@ -481,7 +481,7 @@ HTML = """theme-selector

ui faces

-
faceforegroundbackgroundstylepreview
+
face △foreground △background △stylepreview
@@ -496,7 +496,7 @@ HTML = """theme-selector
-
facefgbgstyleinheritsizecontrast
+
face △fg △bg △styleinherit △size △contrast △
@@ -750,6 +750,7 @@ function buildPkgTable(){ const cr=document.createElement('td');const rb=document.createElement('button');rb.className='sbtn';rb.textContent='↺';rb.title='reset to default';rb.onclick=()=>{PKGMAP[app][face]=seedFace(def);pkgChanged();};cr.appendChild(rb); tr.append(c0,cf,cb,cw,ci,ch,cc,cr);tb.appendChild(tr); } + applyTableSort('pkgbody'); } function ofs(app,face){const f=PKGMAP[app][face]||{},fg=pkgEffFg(app,face)||MAP['p'],bg=pkgEffBg(app,face);const dec=(f.underline?'underline ':'')+(f.strike?'line-through':'');return `color:${fg};${bg?'background:'+bg+';':''}font-weight:${f.bold?'bold':'normal'};font-style:${f.italic?'italic':'normal'};text-decoration:${dec.trim()||'none'};font-size:${(f.height||1)}em`;} function os(app,face,txt){return `${txt}`;} @@ -1064,12 +1065,21 @@ function buildUITable(){ const cP=document.createElement('td');cP.className='ex';cP.id='uiprev-'+face;cP.textContent=ex;cP.style.padding='4px 10px';cP.style.borderRadius='4px'; tr.appendChild(c0);tr.appendChild(cF);tr.appendChild(cB);tr.appendChild(cS);tr.appendChild(cP);tb.appendChild(tr);paintUI(face); } + applyTableSort('uibody'); } let D={}; function srt(c){const tb=document.getElementById('legbody');const r=[...tb.rows];D[c]=!D[c]; r.sort((a,b)=>{const x=(c===0?a.querySelector('select').value:a.cells[0].innerText).toLowerCase(), y=(c===0?b.querySelector('select').value:b.cells[0].innerText).toLowerCase(); return (xy?1:0)*(D[c]?1:-1);});r.forEach(x=>tb.appendChild(x));} +// Generic header-click sort for the package and UI tables. Reads a select +// value, a numeric input, or cell text (numeric when the text leads with a +// number, e.g. contrast or size). The sort is remembered per table and +// re-applied after a rebuild so editing a face does not reset it. +let tableSort={}; +function cellVal(td){if(!td)return '';const s=td.querySelector('select');if(s)return s.value.toLowerCase();const i=td.querySelector('input');if(i)return parseFloat(i.value)||0;const t=td.innerText.trim();const n=parseFloat(t);return (!isNaN(n)&&/^[-\\d.]/.test(t))?n:t.toLowerCase();} +function srtTable(tbId,col){tableSort[tbId]={col,asc:!(tableSort[tbId]&&tableSort[tbId].col===col&&tableSort[tbId].asc)};applyTableSort(tbId);} +function applyTableSort(tbId){const s=tableSort[tbId];if(!s)return;const tb=document.getElementById(tbId);if(!tb)return;const dir=s.asc?1:-1;const r=[...tb.rows];r.sort((a,b)=>{const x=cellVal(a.cells[s.col]),y=cellVal(b.cells[s.col]);return ((typeof x==='number'&&typeof y==='number')?x-y:(xy?1:0))*dir;});r.forEach(x=>tb.appendChild(x));} buildLangSel();buildAppSel();renderPalette();buildTable();buildUITable();renderCode();applyGround();updateTitle();initPicker();buildPkgTable();buildPkgPreview();syncMockHeight();syncPkgHeight(); addEventListener('resize',()=>{syncMockHeight();syncPkgHeight();}); // Phase-1 self-test (open with #selftest): seed -> export -> import -> compare. diff --git a/scripts/theme-selector/theme-selector.html b/scripts/theme-selector/theme-selector.html index b64fcded..30279c74 100644 --- a/scripts/theme-selector/theme-selector.html +++ b/scripts/theme-selector/theme-selector.html @@ -110,7 +110,7 @@

ui faces

-
faceforegroundbackgroundstylepreview
+
face △foreground △background △stylepreview
@@ -125,7 +125,7 @@
-
facefgbgstyleinheritsizecontrast
+
face △fg △bg △styleinherit △size △contrast △
@@ -379,6 +379,7 @@ function buildPkgTable(){ const cr=document.createElement('td');const rb=document.createElement('button');rb.className='sbtn';rb.textContent='↺';rb.title='reset to default';rb.onclick=()=>{PKGMAP[app][face]=seedFace(def);pkgChanged();};cr.appendChild(rb); tr.append(c0,cf,cb,cw,ci,ch,cc,cr);tb.appendChild(tr); } + applyTableSort('pkgbody'); } function ofs(app,face){const f=PKGMAP[app][face]||{},fg=pkgEffFg(app,face)||MAP['p'],bg=pkgEffBg(app,face);const dec=(f.underline?'underline ':'')+(f.strike?'line-through':'');return `color:${fg};${bg?'background:'+bg+';':''}font-weight:${f.bold?'bold':'normal'};font-style:${f.italic?'italic':'normal'};text-decoration:${dec.trim()||'none'};font-size:${(f.height||1)}em`;} function os(app,face,txt){return `${txt}`;} @@ -693,12 +694,21 @@ function buildUITable(){ const cP=document.createElement('td');cP.className='ex';cP.id='uiprev-'+face;cP.textContent=ex;cP.style.padding='4px 10px';cP.style.borderRadius='4px'; tr.appendChild(c0);tr.appendChild(cF);tr.appendChild(cB);tr.appendChild(cS);tr.appendChild(cP);tb.appendChild(tr);paintUI(face); } + applyTableSort('uibody'); } let D={}; function srt(c){const tb=document.getElementById('legbody');const r=[...tb.rows];D[c]=!D[c]; r.sort((a,b)=>{const x=(c===0?a.querySelector('select').value:a.cells[0].innerText).toLowerCase(), y=(c===0?b.querySelector('select').value:b.cells[0].innerText).toLowerCase(); return (xy?1:0)*(D[c]?1:-1);});r.forEach(x=>tb.appendChild(x));} +// Generic header-click sort for the package and UI tables. Reads a select +// value, a numeric input, or cell text (numeric when the text leads with a +// number, e.g. contrast or size). The sort is remembered per table and +// re-applied after a rebuild so editing a face does not reset it. +let tableSort={}; +function cellVal(td){if(!td)return '';const s=td.querySelector('select');if(s)return s.value.toLowerCase();const i=td.querySelector('input');if(i)return parseFloat(i.value)||0;const t=td.innerText.trim();const n=parseFloat(t);return (!isNaN(n)&&/^[-\d.]/.test(t))?n:t.toLowerCase();} +function srtTable(tbId,col){tableSort[tbId]={col,asc:!(tableSort[tbId]&&tableSort[tbId].col===col&&tableSort[tbId].asc)};applyTableSort(tbId);} +function applyTableSort(tbId){const s=tableSort[tbId];if(!s)return;const tb=document.getElementById(tbId);if(!tb)return;const dir=s.asc?1:-1;const r=[...tb.rows];r.sort((a,b)=>{const x=cellVal(a.cells[s.col]),y=cellVal(b.cells[s.col]);return ((typeof x==='number'&&typeof y==='number')?x-y:(xy?1:0))*dir;});r.forEach(x=>tb.appendChild(x));} buildLangSel();buildAppSel();renderPalette();buildTable();buildUITable();renderCode();applyGround();updateTitle();initPicker();buildPkgTable();buildPkgPreview();syncMockHeight();syncPkgHeight(); addEventListener('resize',()=>{syncMockHeight();syncPkgHeight();}); // Phase-1 self-test (open with #selftest): seed -> export -> import -> compare. -- cgit v1.2.3