aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-selector
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-07 16:45:31 -0500
committerCraig Jennings <c@cjennings.net>2026-06-07 16:45:31 -0500
commitb0393b8e851f3f4e8355f0e513e9129bfc115611 (patch)
treed8c125975dd123bfb236cbcdd2cedda5543b984b /scripts/theme-selector
parent350b5e140eb8b5e9b0de87065ad341423f6aa0bf (diff)
downloaddotemacs-b0393b8e851f3f4e8355f0e513e9129bfc115611.tar.gz
dotemacs-b0393b8e851f3f4e8355f0e513e9129bfc115611.zip
feat(theme-selector): add browser-based theme design tool
A self-contained tool for building Emacs color themes by eye. generate.py emits one HTML page with six languages of tree-sitter-tokenized code, a category-to-color assignment table, a UI-faces table, and an editable palette. Reassign colors from the palette, toggle weight and slant per category, set foreground and background per UI face, then export a theme.json a later build step turns into theme files. The export carries the name, palette, syntax assignments, bold and italic sets, and a ui object of per-face foreground and background. The theme name is both the json name field and the download filename. samples.py holds the language samples and the default color map. theme-selector.html is the generated output. The json-to-theme converter is the next piece, and the part worth TDD.
Diffstat (limited to 'scripts/theme-selector')
-rw-r--r--scripts/theme-selector/README.md67
-rw-r--r--scripts/theme-selector/generate.py209
-rw-r--r--scripts/theme-selector/samples.py168
-rw-r--r--scripts/theme-selector/theme-selector.html160
4 files changed, 604 insertions, 0 deletions
diff --git a/scripts/theme-selector/README.md b/scripts/theme-selector/README.md
new file mode 100644
index 00000000..a8be0a10
--- /dev/null
+++ b/scripts/theme-selector/README.md
@@ -0,0 +1,67 @@
+# theme-selector
+
+A self-contained tool for designing Emacs color themes by eye. It renders six
+languages of tree-sitter-tokenized code, a category→color assignment table, a
+UI-faces table, and an editable palette into one HTML page you drive in the
+browser. Reassign colors, toggle weight/slant, edit the palette, then export a
+`theme.json` that a build step turns into `themes/<name>-*.el`.
+
+## Run
+
+```bash
+python3 generate.py # writes theme-selector.html beside this script
+```
+
+Then open it in Chrome (Firefox had color-rendering flakiness during design):
+
+```bash
+WAYLAND_DISPLAY=wayland-1 google-chrome-stable theme-selector.html
+```
+
+During color work, disable Hyprland inactive-window dimming so colors read true:
+
+```bash
+hyprctl keyword decoration:dim_inactive false
+```
+
+## Files
+
+- `generate.py` — emits the HTML+JS. Edit here to change layout or behavior.
+- `samples.py` — the language code samples and the default category→color map
+ (`COLS`). `generate.py` reads the part before the `cols=` marker.
+- `theme-selector.html` — generated output. Regenerate; don't hand-edit.
+
+## What it captures
+
+- Background and foreground (the `default` face's `:background` / `:foreground`).
+- The syntax layer: every font-lock / tree-sitter category (keyword, string,
+ function, type, comment, and the rest), each with normal/bold/italic.
+- UI faces: cursor, region, mode-line, fringe, line numbers, isearch, paren
+ match, link, error/warning/success, and more — foreground and background per
+ face.
+- The palette itself: add by hex or swatch, remove, rename, drag to reorder.
+
+## theme.json contract
+
+The export (and what a build step consumes):
+
+```json
+{
+ "name": "dupre-revision",
+ "palette": [["#67809c", "blue"], ["#e8bd30", "gold"]],
+ "assignments": {"kw": "#67809c", "str": "#5d9b86", "bg": "#0d0b0a", "p": "#cdced1"},
+ "bold": ["kw", "fnd"],
+ "italic": [],
+ "ui": {"region": {"fg": null, "bg": "#264364"}, "cursor": {"fg": null, "bg": "#a9b2bb"}}
+}
+```
+
+The theme name is both the `name` field and the download filename
+(`<name>.json`, sanitized). Upload a `theme.json` to start from a prior theme.
+
+## Next step (not yet built)
+
+A `theme.json` → `themes/<name>-palette.el` + `-faces.el` + `-theme.el`
+converter. That step is the correctness-sensitive part and is the one worth
+TDD: JSON in, valid Emacs palette + faces out, every face mapped, WCAG-contrast
+asserted on the result.
diff --git a/scripts/theme-selector/generate.py b/scripts/theme-selector/generate.py
new file mode 100644
index 00000000..94910d1c
--- /dev/null
+++ b/scripts/theme-selector/generate.py
@@ -0,0 +1,209 @@
+import json, re, os
+HERE=os.path.dirname(os.path.abspath(__file__))
+ns={}
+src=open(os.path.join(HERE,'samples.py')).read()
+exec(src[:src.index('cols=')], ns)
+SAMPLES={"Elisp":ns['ELS'],"Go":ns['GOS'],"Python":ns['PYS'],"TypeScript":ns['TSS'],"Shell":ns['SHS'],"C/C++":ns['CS']}
+COLS=ns['COLS']
+MAP={k:v[0] for k,v in COLS.items()}; BOLD={k:v[1] for k,v in COLS.items()}; MAP['str']='#5d9b86'; MAP['bg']='#0d0b0a'
+PALETTE=[["#67809c","blue"],["#e8bd30","gold"],["#9b5fd0","regal"],["#2ba178","emerald"],["#5d9b86","sage"],
+ ["#cb6b4d","terracotta"],["#be9e74","tan"],["#cdced1","white"],["#a9b2bb","silver"],["#838d97","steel"],
+ ["#5e6770","pewter"],["#2f343a","gunmetal"],["#264364","navy"],["#0d0b0a","ground"],["#1a1714","bg-dim"]]
+CATS=[["bg","background (ground)","Aa Bb 123"],["p","fg · default text","other / whitespace"],["kw","keyword","class def if return"],["bi","builtin","len echo printf"],
+ ["pp","preprocessor","#include #define"],["fnd","function · def","resolve push"],
+ ["fnc","function · call","printf rsync get"],["dec","decorator","@dataclass"],
+ ["ty","type / class","int str Order Queue"],["prop","property / field","id name items"],
+ ["con","constant","None nil NULL true"],["num","number","8080 100 -1"],
+ ["str","string",'"dupre" "fmt"'],["esc","escape","\\n \\t"],["re","regexp","/^#[0-9a-f]+/"],
+ ["doc","docstring",'"""..."""'],["cm","comment","# reject nil"],["cmd","comment delim","# // ;;"],
+ ["var","variable / use","value key self"],["op","operator",": = -> =="],
+ ["punc","punctuation","{ } ( ) ;"]]
+UI_FACES=[["cursor","cursor","Aa|"],["region","region (selection)","selected text"],
+ ["hl-line","hl-line (current line)","current line"],["highlight","highlight","hover"],
+ ["mode-line","mode-line","status active"],["mode-line-inactive","mode-line-inactive","status idle"],
+ ["fringe","fringe","| |"],["line-number","line-number"," 42"],
+ ["line-number-current-line","line-number-current-line","> 42"],["minibuffer-prompt","minibuffer-prompt","M-x "],
+ ["isearch","isearch (match)","match"],["lazy-highlight","lazy-highlight","other match"],
+ ["isearch-fail","isearch-fail","no match"],["show-paren-match","show-paren-match","( )"],
+ ["show-paren-mismatch","show-paren-mismatch",") ("],["link","link","https://"],
+ ["error","error","error!"],["warning","warning","warning"],
+ ["success","success","ok"],["vertical-border","vertical-border","|"]]
+UIMAP={"cursor":{"fg":None,"bg":"#a9b2bb"},"region":{"fg":None,"bg":"#264364"},
+ "hl-line":{"fg":None,"bg":"#1a1714"},"highlight":{"fg":None,"bg":"#2f343a"},
+ "mode-line":{"fg":"#cdced1","bg":"#2f343a"},"mode-line-inactive":{"fg":"#838d97","bg":"#1a1714"},
+ "fringe":{"fg":None,"bg":"#0d0b0a"},"line-number":{"fg":"#5e6770","bg":None},
+ "line-number-current-line":{"fg":"#e8bd30","bg":"#1a1714"},"minibuffer-prompt":{"fg":"#67809c","bg":None},
+ "isearch":{"fg":"#0d0b0a","bg":"#e8bd30"},"lazy-highlight":{"fg":"#0d0b0a","bg":"#838d97"},
+ "isearch-fail":{"fg":"#cb6b4d","bg":None},"show-paren-match":{"fg":None,"bg":"#264364"},
+ "show-paren-mismatch":{"fg":"#0d0b0a","bg":"#cb6b4d"},"link":{"fg":"#67809c","bg":None},
+ "error":{"fg":"#cb6b4d","bg":None},"warning":{"fg":"#e8bd30","bg":None},
+ "success":{"fg":"#5d9b86","bg":None},"vertical-border":{"fg":"#2f343a","bg":None}}
+def cid(l): return re.sub(r'\W','',l)
+code_cont="".join(f'<div class="col"><h2>{l}</h2><pre id="code-{cid(l)}"></pre></div>' for l in SAMPLES)
+HTML = """<!doctype html><meta charset=utf-8><title>theme-selector</title>
+<style>
+ body{background:#0d0b0a;color:#cdced1;font:15px/1.55 monospace;margin:20px}
+ h1{font-size:22px;font-weight:normal;color:#e8bd30;margin:26px 0 10px;border-bottom:1px solid #252321;padding-bottom:6px}
+ h2{font-size:10pt;color:#8a9496;font-weight:normal;margin:0 0 4px}
+ .wrap{display:flex;flex-wrap:nowrap;overflow-x:auto;gap:14px;padding-bottom:10px}
+ .col{flex:0 0 auto;width:460px}
+ pre{background:#0d0b0a;border:1px solid #252321;border-radius:8px;padding:14px 16px;font-size:19px;overflow:auto;white-space:pre}
+ table.leg{border-collapse:collapse} table.leg td{padding:4px 12px;vertical-align:middle}
+ 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}
+ .cat{color:#b4b1a2} .ex{font-size:17px}
+ .sbtn{width:26px;height:24px;border:1px solid #3a3a3a;border-radius:3px;background:#eaeaea;color:#111;cursor:pointer;font-size:15px;margin-right:2px;padding:0}
+ .sbtn.on{background:#0d0b0a;color:#cdced1;border-color:#8a9496}
+ .pals{display:flex;gap:8px;flex-wrap:wrap}
+ .pchip{width:128px;height:58px;border-radius:6px;border:1px solid #00000060;position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:grab}
+ .pchip.drag{opacity:.4} .pchip input.nm{background:transparent;border:none;text-align:center;font:bold 10pt monospace;width:108px;outline:none}
+ .pchip .hx{font-size:10pt;opacity:.8} .pchip .rm{position:absolute;top:2px;right:5px;background:none;border:none;cursor:pointer;font-size:14px;font-weight:bold;opacity:.7}
+ .palctl{margin-top:12px;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
+ .palctl input[type=text]{background:#161412;border:1px solid #252321;color:#cdced1;border-radius:4px;padding:5px 8px;font:10pt monospace}
+ .palctl input[type=text]::placeholder{color:#b4b1a2;opacity:1}
+ .palctl input[type=color]{width:128px;height:58px;border:1px solid #00000060;border-radius:6px;padding:2px;cursor:pointer}
+ .palctl button,.filebar button,.fbtn{background:#252321;color:#e8bd30;border:1px solid #3a3a3a;border-radius:4px;padding:6px 12px;font:10pt monospace;cursor:pointer}
+ #export{width:100%;height:180px;margin-top:10px;background:#0d0b0a;color:#a4ac64;border:1px solid #252321;border-radius:6px;font:10pt monospace;padding:10px}
+ .filebar{margin:6px 0 0;display:flex;gap:8px;align-items:center}
+ #pagetitle{font-size:30px;color:#cdced1;font-weight:normal;border:none;margin:4px 0 18px;padding:0}
+</style>
+<h1 id="pagetitle">Untitled: color palette</h1>
+<h1>code samples</h1>
+<div class="wrap">CODE_CONT</div>
+<h1>color &rarr; category — chip reassigns · N/B/I sets weight &amp; slant · click a header to sort</h1>
+<table class="leg" id="legtable"><thead><tr><th onclick="srt(0)">color &#9651;</th><th>style</th><th onclick="srt(1)">category &#9651;</th><th>example</th></tr></thead><tbody id="legbody"></tbody></table>
+<h1>UI / interface faces — foreground &amp; background per face</h1>
+<table class="leg" id="uitable"><thead><tr><th>face</th><th>foreground</th><th>background</th><th>preview</th></tr></thead><tbody id="uibody"></tbody></table>
+<h1>palette — add / remove / rename / drag to reorder</h1>
+<div class="pals" id="pals"></div>
+<div class="palctl">
+ <input type="color" id="newhex" value="#888888" oninput="syncHex('swatch')">
+ <input type="text" id="newhexstr" placeholder="#rrggbb" value="#888888" oninput="syncHex('text')" style="width:110px">
+ <input type="text" id="newname" placeholder="name">
+ <button onclick="addColor()">+ add color</button>
+</div>
+<h1>save / load theme</h1>
+<div class="filebar">
+ <label style="color:#b4b1a2">theme name</label><input type="text" id="themename" value="" placeholder="untitled" oninput="updateTitle()" style="background:#161412;border:1px solid #252321;color:#cdced1;border-radius:4px;padding:5px 8px;font:10pt monospace;width:200px">
+</div>
+<div class="filebar">
+ <button onclick="download()">&#11015; download &lt;name&gt;.json</button>
+ <label class="fbtn">&#11014; load theme.json<input type="file" accept=".json" onchange="importFile(event)" style="display:none"></label>
+ <button id="jsonbtn" onclick="toggleJSON()">show JSON</button>
+</div>
+<textarea id="export" style="display:none" readonly></textarea>
+<script>
+const SAMPLES=SAMPLES_J, CATS=CATS_J, UI_FACES=UIFACES_J;
+let MAP=MAP_J, PALETTE=PALETTE_J, BOLD=BOLD_J, ITALIC={}, UIMAP=UIMAP_J;
+function esc(t){return t.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
+function lin(c){c/=255;return c<=0.03928?c/12.92:Math.pow((c+0.055)/1.055,2.4);}
+function rl(h){return 0.2126*lin(parseInt(h.substr(1,2),16))+0.7152*lin(parseInt(h.substr(3,2),16))+0.0722*lin(parseInt(h.substr(5,2),16));}
+function textOn(h){const L=rl(h);return ((L+0.05)/0.05)>(1.05/(L+0.05))?'#000':'#fff';}
+function cid(l){return l.replace(/\\W/g,'');}
+function renderCode(){
+ for(const lang in SAMPLES){let html='';
+ for(const line of SAMPLES[lang]){
+ if(line.length===0){html+='\\n';continue;}
+ for(const [k,t] of line){const c=MAP[k]||'#cdced1';const w=BOLD[k]?'bold':'normal';const s=ITALIC[k]?'italic':'normal';
+ html+=`<span style="color:${c};font-weight:${w};font-style:${s}">${esc(t)}</span>`;}
+ html+='\\n';}
+ document.getElementById('code-'+cid(lang)).innerHTML=html;}
+}
+function buildTable(){
+ const tb=document.getElementById('legbody');tb.innerHTML='';
+ for(const [kind,label,ex] of CATS){
+ const tr=document.createElement('tr');
+ const sel=document.createElement('select');sel.className='chip';
+ const cur=MAP[kind];const have=PALETTE.some(p=>p[0]===cur);
+ const list=have?PALETTE:[[cur,'(gone) '+cur],...PALETTE];
+ for(const [hex,name] of list){const o=document.createElement('option');o.value=hex;o.textContent=name+' '+hex;o.style.background=hex;o.style.color=textOn(hex);sel.appendChild(o);}
+ sel.value=cur;
+ const exTd=document.createElement('td');exTd.className='ex';exTd.textContent=ex;
+ function styleChip(){sel.style.background=sel.value;sel.style.color=textOn(sel.value);}
+ function styleEx(){exTd.style.color=(kind==='bg'?MAP['p']:MAP[kind]);exTd.style.background=MAP['bg'];exTd.style.fontWeight=BOLD[kind]?'bold':'normal';exTd.style.fontStyle=ITALIC[kind]?'italic':'normal';}
+ styleChip();styleEx();
+ sel.onchange=()=>{MAP[kind]=sel.value;styleChip();styleEx();renderCode();if(kind==='bg')applyGround();};
+ // style buttons
+ const stTd=document.createElement('td');
+ if(kind!=='bg'){const defs=[['N','a','normal'],['B','a','bold'],['I','a','italic']];
+ const btns={};
+ defs.forEach(([id,ch,mode])=>{const b=document.createElement('button');b.className='sbtn';b.style.fontWeight=mode==='bold'?'bold':'normal';b.style.fontStyle=mode==='italic'?'italic':'normal';b.textContent=ch;
+ b.onclick=()=>{if(mode==='normal'){BOLD[kind]=false;ITALIC[kind]=false;}else if(mode==='bold'){BOLD[kind]=!BOLD[kind];}else{ITALIC[kind]=!ITALIC[kind];}refresh();renderCode();styleEx();};
+ btns[mode]=b;stTd.appendChild(b);});
+ function refresh(){btns.normal.classList.toggle('on',!BOLD[kind]&&!ITALIC[kind]);btns.bold.classList.toggle('on',!!BOLD[kind]);btns.italic.classList.toggle('on',!!ITALIC[kind]);}
+ refresh();}
+ const c0=document.createElement('td');c0.appendChild(sel);
+ const c2=document.createElement('td');c2.className='cat';c2.textContent=label;
+ tr.appendChild(c0);tr.appendChild(stTd);tr.appendChild(c2);tr.appendChild(exTd);
+ tb.appendChild(tr);}
+}
+let dragFrom=null;
+function renderPalette(){
+ const p=document.getElementById('pals');p.innerHTML='';
+ PALETTE.forEach((pc,i)=>{const [hex,name]=pc;const tc=textOn(hex);
+ const d=document.createElement('div');d.className='pchip';d.style.background=hex;d.draggable=true;
+ d.innerHTML=`<button class="rm" title="remove" style="color:${tc}">×</button><input class="nm" value="${name}" style="color:${tc}"><div class="hx" style="color:${tc}">${hex}</div>`;
+ d.querySelector('.rm').onclick=()=>{PALETTE.splice(i,1);renderPalette();buildTable();};
+ d.querySelector('.nm').onchange=(e)=>{PALETTE[i][1]=e.target.value;buildTable();buildUITable();};
+ d.ondragstart=()=>{dragFrom=i;d.classList.add('drag');};
+ d.ondragend=()=>d.classList.remove('drag');
+ d.ondragover=(e)=>e.preventDefault();
+ d.ondrop=(e)=>{e.preventDefault();if(dragFrom===null||dragFrom===i)return;const m=PALETTE.splice(dragFrom,1)[0];PALETTE.splice(i,0,m);dragFrom=null;renderPalette();buildTable();};
+ p.appendChild(d);});
+ buildUITable();
+}
+function normHex(s){s=s.trim();if(/^[0-9a-fA-F]{6}$/.test(s))s='#'+s;return /^#[0-9a-fA-F]{6}$/.test(s)?s.toLowerCase():null;}
+function syncHex(src){const sw=document.getElementById('newhex'),tx=document.getElementById('newhexstr');
+ if(src==='swatch'){tx.value=sw.value;}else{const h=normHex(tx.value);if(h)sw.value=h;}}
+function addColor(){const h=normHex(document.getElementById('newhexstr').value)||document.getElementById('newhex').value;const name=document.getElementById('newname').value||h;PALETTE.push([h,name]);document.getElementById('newname').value='';renderPalette();buildTable();}
+function themeName(){return (document.getElementById('themename').value||'theme').trim()||'theme';}
+function fileSlug(){return themeName().replace(/[^A-Za-z0-9._-]+/g,'-').replace(/^-+|-+$/g,'')||'theme';}
+function exportObj(){const a={};CATS.forEach(c=>a[c[0]]=MAP[c[0]]);return {name:themeName(),palette:PALETTE,assignments:a,bold:Object.keys(BOLD).filter(k=>BOLD[k]),italic:Object.keys(ITALIC).filter(k=>ITALIC[k]),ui:UIMAP};}
+function exportState(){const t=document.getElementById('export');t.value=JSON.stringify(exportObj(),null,1);t.style.display='block';t.focus();t.select();}
+function toggleJSON(){const t=document.getElementById('export'),b=document.getElementById('jsonbtn');if(t.style.display==='block'){t.style.display='none';b.textContent='show JSON';}else{exportState();b.textContent='hide JSON';}}
+function updateTitle(){const n=document.getElementById('themename').value.trim();document.getElementById('pagetitle').textContent=(n||'Untitled')+': color palette';}
+function download(){const blob=new Blob([JSON.stringify(exportObj(),null,1)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=fileSlug()+'.json';a.click();}
+function importFile(ev){const f=ev.target.files[0];if(!f)return;const r=new FileReader();
+ r.onload=()=>{try{const d=JSON.parse(r.result);if(d.name)document.getElementById('themename').value=d.name;if(d.palette)PALETTE=d.palette;if(d.assignments)Object.assign(MAP,d.assignments);
+ BOLD={};(d.bold||[]).forEach(k=>BOLD[k]=true);ITALIC={};(d.italic||[]).forEach(k=>ITALIC[k]=true);
+ if(d.ui)Object.assign(UIMAP,d.ui);
+ renderPalette();buildTable();buildUITable();renderCode();applyGround();updateTitle();}catch(e){alert('bad theme file: '+e.message);}};
+ r.readAsText(f);ev.target.value='';}
+function applyGround(){document.querySelectorAll('pre').forEach(p=>p.style.background=MAP['bg']);document.querySelectorAll('.ex').forEach(e=>e.style.background=MAP['bg']);}
+function uiSelect(face,attr){
+ const sel=document.createElement('select');sel.className='chip';
+ const none=document.createElement('option');none.value='';none.textContent='— none —';none.style.background='#161412';none.style.color='#b4b1a2';sel.appendChild(none);
+ for(const [hex,name] of PALETTE){const o=document.createElement('option');o.value=hex;o.textContent=name+' '+hex;o.style.background=hex;o.style.color=textOn(hex);sel.appendChild(o);}
+ sel.value=UIMAP[face][attr]||'';
+ function style(){if(sel.value){sel.style.background=sel.value;sel.style.color=textOn(sel.value);}else{sel.style.background='#161412';sel.style.color='#b4b1a2';}}
+ style();
+ sel.onchange=()=>{UIMAP[face][attr]=sel.value||null;style();paintUI(face);};
+ return sel;
+}
+function paintUI(face){const pv=document.getElementById('uiprev-'+face);if(!pv)return;pv.style.color=UIMAP[face].fg||MAP['p'];pv.style.background=UIMAP[face].bg||MAP['bg'];}
+function buildUITable(){
+ const tb=document.getElementById('uibody');tb.innerHTML='';
+ for(const [face,label,ex] of UI_FACES){
+ const tr=document.createElement('tr');
+ const c0=document.createElement('td');c0.className='cat';c0.textContent=label;
+ const cF=document.createElement('td');cF.appendChild(uiSelect(face,'fg'));
+ const cB=document.createElement('td');cB.appendChild(uiSelect(face,'bg'));
+ 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(cP);tb.appendChild(tr);paintUI(face);
+ }
+}
+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[2].innerText).toLowerCase(),
+ y=(c===0?b.querySelector('select').value:b.cells[2].innerText).toLowerCase();
+ return (x<y?-1:x>y?1:0)*(D[c]?1:-1);});r.forEach(x=>tb.appendChild(x));}
+renderPalette();buildTable();buildUITable();renderCode();applyGround();updateTitle();
+</script>"""
+HTML=(HTML.replace("CODE_CONT",code_cont).replace("SAMPLES_J",json.dumps(SAMPLES))
+ .replace("PALETTE_J",json.dumps(PALETTE)).replace("CATS_J",json.dumps(CATS))
+ .replace("UIFACES_J",json.dumps(UI_FACES)).replace("UIMAP_J",json.dumps(UIMAP))
+ .replace("BOLD_J",json.dumps(BOLD)).replace("MAP_J",json.dumps(MAP)))
+OUT=os.path.join(HERE,'theme-selector.html')
+open(OUT,"w").write(HTML)
+print("wrote",OUT)
diff --git a/scripts/theme-selector/samples.py b/scripts/theme-selector/samples.py
new file mode 100644
index 00000000..1637a2a3
--- /dev/null
+++ b/scripts/theme-selector/samples.py
@@ -0,0 +1,168 @@
+GROUND="#0d0b0a"
+COLS={
+ 'kw':("#67809c",True),'bi':("#67809c",False),'pp':("#67809c",False),
+ 'fnd':("#a9b2bb",True),'fnc':("#a9b2bb",False),'dec':("#e8bd30",False),
+ 'ty':("#9b5fd0",False),'prop':("#838d97",False),
+ 'con':("#cb6b4d",False),'num':("#cb6b4d",False),'esc':("#cb6b4d",False),
+ 'str':("#2ba178",False),'re':("#5d9b86",False),'doc':("#5d9b86",False),
+ 'cm':("#be9e74",False),'cmd':("#a9b2bb",False),
+ 'var':("#e8bd30",False),'op':("#a9b2bb",False),'punc':("#a9b2bb",False),'p':("#cdced1",False),
+}
+NAMES={"#67809c":"blue","#e8bd30":"gold","#9b5fd0":"regal","#2ba178":"emerald","#cb6b4d":"terracotta","#be9e74":"tan","#5d9b86":"sage","#cdced1":"white","#a9b2bb":"silver","#838d97":"steel","#5e6770":"pewter","#2f343a":"gunmetal","#264364":"navy"}
+def esc(t): return t.replace("&","&amp;").replace("<","&lt;").replace(">","&gt;")
+def span(k,t):
+ c,b=COLS[k]; w=";font-weight:bold" if b else ""
+ return f'<span style="color:{c}{w}">{esc(t)}</span>'
+def render(lines): return "\n".join("".join(span(k,t) for k,t in ln) or "&nbsp;" for ln in lines)
+
+PYS=[
+ [('cmd','#'),('cm',' theme.py')],
+ [('kw','from'),('p',' '),('var','dataclasses'),('p',' '),('kw','import'),('p',' '),('var','dataclass'),('punc',','),('p',' '),('var','field')],
+ [],
+ [('con','DEFAULT_PORT'),('op',':'),('p',' '),('ty','int'),('p',' '),('op','='),('p',' '),('num','8080')],
+ [],
+ [('dec','@dataclass')],
+ [('kw','class'),('p',' '),('ty','Theme'),('op',':')],
+ [('p',' '),('doc','"""A color theme."""')],
+ [('p',' '),('prop','name'),('op',':'),('p',' '),('ty','str'),('p',' '),('op','='),('p',' '),('str','"dupre"')],
+ [('p',' '),('prop','colors'),('op',':'),('p',' '),('ty','dict'),('p',' '),('op','='),('p',' '),('fnc','field'),('punc','('),('prop','default_factory'),('op','='),('ty','dict'),('punc',')')],
+ [],
+ [('p',' '),('kw','def'),('p',' '),('fnd','resolve'),('punc','('),('var','self'),('punc',','),('p',' '),('var','key'),('op',':'),('p',' '),('ty','str'),('punc',')'),('p',' '),('op','->'),('p',' '),('ty','str'),('p',' '),('op','|'),('p',' '),('con','None'),('op',':')],
+ [('p',' '),('cmd','#'),('cm',' fallback to none')],
+ [('p',' '),('var','v'),('p',' '),('op','='),('p',' '),('var','self'),('op','.'),('prop','colors'),('op','.'),('fnc','get'),('punc','('),('var','key'),('punc',','),('p',' '),('str','"'),('esc','\\t'),('str','none"'),('punc',')')],
+ [('p',' '),('kw','if'),('p',' '),('bi','len'),('punc','('),('var','v'),('punc',')'),('p',' '),('op','=='),('p',' '),('num','0'),('op',':'),('p',' '),('kw','return'),('p',' '),('con','None')],
+ [('p',' '),('kw','return'),('p',' '),('var','v')],
+]
+ELS=[
+ [('cmd',';;'),('cm',' cache.el')],
+ [('punc','('),('kw','require'),('p',' '),('con',"'cl-lib"),('punc',')')],
+ [],
+ [('punc','('),('kw','defvar'),('p',' '),('var','cache--tbl'),('p',' '),('punc','('),('fnc','make-hash-table'),('p',' '),('con',':test'),('p',' '),('con',"'equal"),('punc','))')],
+ [('p',' '),('doc','"Memo table.")')],
+ [],
+ [('punc','('),('kw','defun'),('p',' '),('fnd','cache-get'),('p',' '),('punc','('),('var','key'),('punc',')')],
+ [('p',' '),('doc','"Return cached value for KEY."')],
+ [('p',' '),('punc','('),('kw','or'),('p',' '),('punc','('),('fnc','gethash'),('p',' '),('var','key'),('p',' '),('var','cache--tbl'),('punc',')')],
+ [('p',' '),('punc','('),('kw','let'),('p',' '),('punc','(('),('var','v'),('p',' '),('punc','('),('fnc','compute'),('p',' '),('var','key'),('p',' '),('num','42'),('punc','))) ')],
+ [('p',' '),('punc','('),('fnc','puthash'),('p',' '),('var','key'),('p',' '),('var','v'),('p',' '),('var','cache--tbl'),('punc',') '),('var','v'),('punc','))))')],
+]
+GOS=[
+ [('cmd','//'),('cm',' queue.go')],
+ [('kw','package'),('p',' '),('var','main')],
+ [],
+ [('kw','import'),('p',' '),('str','"fmt"')],
+ [],
+ [('kw','const'),('p',' '),('con','MaxItems'),('p',' '),('op','='),('p',' '),('num','100')],
+ [],
+ [('kw','type'),('p',' '),('ty','Order'),('p',' '),('kw','struct'),('p',' '),('punc','{')],
+ [('p',' '),('prop','ID'),('p',' '),('ty','int')],
+ [('p',' '),('prop','Name'),('p',' '),('ty','string')],
+ [('punc','}')],
+ [],
+ [('kw','func'),('p',' '),('punc','('),('var','q'),('p',' '),('op','*'),('ty','Queue'),('punc',')'),('p',' '),('fnd','Push'),('punc','('),('var','o'),('p',' '),('op','*'),('ty','Order'),('punc',')'),('p',' '),('ty','error'),('p',' '),('punc','{')],
+ [('p',' '),('cmd','//'),('cm',' reject nil')],
+ [('p',' '),('kw','if'),('p',' '),('var','o'),('p',' '),('op','=='),('p',' '),('con','nil'),('p',' '),('punc','{')],
+ [('p',' '),('kw','return'),('p',' '),('fnc','fmt.Errorf'),('punc','('),('str','"nil"'),('punc',')')],
+ [('p',' '),('punc','}')],
+ [('p',' '),('var','q'),('op','.'),('prop','items'),('p',' '),('op','='),('p',' '),('fnc','append'),('punc','('),('var','q'),('op','.'),('prop','items'),('punc',','),('p',' '),('var','o'),('punc',')')],
+ [('p',' '),('kw','return'),('p',' '),('con','nil')],
+ [('punc','}')],
+]
+TSS=[
+ [('cmd','//'),('cm',' orders.ts')],
+ [('kw','import'),('p',' '),('punc','{'),('p',' '),('ty','Order'),('p',' '),('punc','}'),('p',' '),('kw','from'),('p',' '),('str','"./types"')],
+ [],
+ [('kw','export'),('p',' '),('kw','interface'),('p',' '),('ty','Queue'),('p',' '),('punc','{')],
+ [('p',' '),('prop','max'),('op',':'),('p',' '),('ty','number'),('punc',';')],
+ [('p',' '),('prop','items'),('op',':'),('p',' '),('ty','Order'),('punc','[];')],
+ [('punc','}')],
+ [],
+ [('dec','@Injectable'),('punc','()')],
+ [('kw','export'),('p',' '),('kw','class'),('p',' '),('ty','OrderQueue'),('p',' '),('kw','implements'),('p',' '),('ty','Queue'),('p',' '),('punc','{')],
+ [('p',' '),('kw','private'),('p',' '),('prop','re'),('p',' '),('op','='),('p',' '),('re','/^#[0-9a-f]{6}$/i'),('punc',';')],
+ [],
+ [('p',' '),('fnd','push'),('punc','('),('var','o'),('op',':'),('p',' '),('ty','Order'),('punc',')'),('op',':'),('p',' '),('ty','boolean'),('p',' '),('punc','{')],
+ [('p',' '),('kw','if'),('p',' '),('punc','('),('var','o'),('p',' '),('op','==='),('p',' '),('con','null'),('punc',')'),('p',' '),('kw','return'),('p',' '),('con','false'),('punc',';')],
+ [('p',' '),('var','console'),('op','.'),('fnc','log'),('punc','('),('str','`id '),('punc','${'),('var','o'),('op','.'),('prop','id'),('punc','}'),('esc','\\n'),('str','`'),('punc',');')],
+ [('p',' '),('kw','return'),('p',' '),('con','true'),('punc',';')],
+ [('p',' '),('punc','}')],
+ [('punc','}')],
+]
+
+CS=[
+ [('cmd','//'),('cm',' theme.c')],
+ [('pp','#include'),('p',' '),('str','<stdio.h>')],
+ [('pp','#define'),('p',' '),('con','MAX_PORT'),('p',' '),('num','8080')],
+ [],
+ [('kw','typedef'),('p',' '),('kw','struct'),('p',' '),('punc','{')],
+ [('p',' '),('ty','int'),('p',' '),('prop','id'),('punc',';')],
+ [('p',' '),('ty','char'),('p',' '),('op','*'),('prop','name'),('punc',';')],
+ [('punc','}'),('p',' '),('ty','Order'),('punc',';')],
+ [],
+ [('ty','int'),('p',' '),('fnd','push'),('punc','('),('ty','Order'),('p',' '),('op','*'),('var','o'),('punc',')'),('p',' '),('punc','{')],
+ [('p',' '),('kw','if'),('p',' '),('punc','('),('var','o'),('p',' '),('op','=='),('p',' '),('con','NULL'),('punc',')'),('p',' '),('punc','{')],
+ [('p',' '),('kw','return'),('p',' '),('num','-1'),('punc',';')],
+ [('p',' '),('punc','}')],
+ [('p',' '),('fnc','printf'),('punc','('),('str','"id=%d'),('esc',chr(92)+'n'),('str','"'),('punc',','),('p',' '),('var','o'),('op','->'),('prop','id'),('punc',');')],
+ [('p',' '),('kw','return'),('p',' '),('num','0'),('punc',';')],
+ [('punc','}')],
+]
+SHS=[
+ [('cmd','#!'),('cm','/bin/bash')],
+ [('cmd','#'),('cm',' deploy.sh')],
+ [('bi','set'),('p',' '),('op','-'),('var','euo'),('p',' '),('var','pipefail')],
+ [],
+ [('var','PORT'),('op','='),('num','8080')],
+ [('var','NAME'),('op','='),('str','"dupre"')],
+ [],
+ [('fnd','deploy'),('punc','()'),('p',' '),('punc','{')],
+ [('p',' '),('kw','local'),('p',' '),('var','target'),('op','='),('str','"$1"')],
+ [('p',' '),('kw','if'),('p',' '),('punc','[['),('p',' '),('op','-z'),('p',' '),('str','"$target"'),('p',' '),('punc',']]'),('punc',';'),('p',' '),('kw','then')],
+ [('p',' '),('bi','echo'),('p',' '),('str','"no target"')],
+ [('p',' '),('kw','return'),('p',' '),('num','1')],
+ [('p',' '),('kw','fi')],
+ [('p',' '),('fnc','rsync'),('p',' '),('op','-az'),('p',' '),('str','"$NAME"'),('p',' '),('str','"$target"')],
+ [('punc','}')],
+]
+
+cols="".join(f'<div class="col"><h2>{n}</h2><pre>{render(s)}</pre></div>' for n,s in [("Elisp",ELS),("Go",GOS),("Python",PYS),("TypeScript",TSS),("Shell",SHS),("C/C++",CS)])
+legend_rows=[
+ ("keyword (bold)","kw","class def if return import"),("builtin","bi","len range print"),
+ ("function — definition (bold)","fnd","resolve cache-get push"),("function — call","fnc","get append fmt.Errorf"),
+ ("decorator / attribute","dec","@dataclass @Injectable"),("type / class","ty","str dict Order Queue boolean"),
+ ("property / field / key","prop","name colors items id re"),("constant","con","None nil true MaxItems :test"),
+ ("number","num","8080 100 42 0"),("string","str",'"dupre" "fmt" `id`'),("escape","esc",r'\t \n'),
+ ("regexp","re",'/^#[0-9a-f]{6}$/i'),("docstring","doc",'"""..." "Memo table."'),
+ ("comment","cm","# reject nil // fallback"),("comment delimiter","cmd","# // ;; /*"),
+ ("variable / use","var","v key self q console"),("operator","op",": = -> | == === . *"),
+ ("punctuation / bracket","punc","{ } ( ) [ ] , ;"),
+]
+def lrow(label,k,ex):
+ c,b=COLS[k]
+ return f'<tr><td class="sw" style="background:{c}"></td><td class="nm">{NAMES.get(c,"")}</td><td class="hx">{c}</td><td class="cat">{label}</td><td class="ex" style="color:{c}{";font-weight:bold" if b else ""}">{esc(ex)}</td></tr>'
+legend="".join(lrow(l,k,e) for l,k,e in legend_rows)
+def grp(title,items):
+ sw="".join(f'<div class="m"><div class="psw" style="background:{h}"></div><div class="lb">{n}<br>{h}</div></div>' for n,h in items)
+ return f'<div class="g"><div class="gt">{title}</div><div class="ramp">{sw}</div></div>'
+palette=(grp("ground / foreground",[("ground","#0d0b0a"),("bg-dim","#1a1714"),("fg","#cdced1")])
+ + grp("syntax hues",[("blue · keyword","#67809c"),("gold · variable","#e8bd30"),("regal · type","#9b5fd0"),("emerald · string","#2ba178"),("terracotta · const/num","#cb6b4d"),("tan · comment","#be9e74")])
+ + grp("metallic greyscale (structural)",[("gunmetal","#2f343a"),("metal","#474e56"),("pewter","#5e6770"),("steel · property","#838d97"),("silver · fn/op/punct","#a9b2bb"),("bright · fg","#cdced1")])
+ + grp("special green + fills",[("muted emerald · doc/regexp","#5d9b86"),("navy fill","#264364"),("gunmetal fill","#2f343a")]))
+html=f'''<!doctype html><meta charset=utf-8><title>dupre revision — canonical</title>
+<style>body{{background:{GROUND};color:#cdced1;font:15px/1.55 monospace;margin:20px}}
+ h1{{font-size:22px;font-weight:normal;color:#e8bd30;margin:26px 0 10px;border-bottom:1px solid #252321;padding-bottom:6px}}
+ h2{{font-size:13px;color:#8a9496;font-weight:normal;margin:0 0 4px}}
+ .wrap{{display:flex;flex-wrap:nowrap;overflow-x:auto;gap:14px;padding-bottom:10px}} .col{{flex:0 0 auto;width:460px}}
+ pre{{background:#0d0b0a;border:1px solid #252321;border-radius:8px;padding:14px 16px;font-size:19px;overflow:auto}}
+ table.leg{{border-collapse:collapse}} table.leg td{{padding:3px 10px;vertical-align:middle}} table.leg th{{cursor:pointer;color:#b4b1a2;text-align:left;padding:3px 10px;user-select:none;font-weight:normal}} table.leg th:hover{{color:#e8bd30}}
+ .sw{{width:26px;height:16px;border-radius:3px;border:1px solid #00000060}} .nm{{color:#c0c5ca;font-size:12px}} .hx{{color:#969385;font-size:11px}} .cat{{color:#b4b1a2}} .ex{{font-size:18px}}
+ .g{{margin:4px 0 10px}} .gt{{color:#8a9496;font-size:12px;margin-bottom:4px}}
+ .ramp{{display:flex;gap:8px;flex-wrap:wrap}} .m{{text-align:center}} .psw{{width:120px;height:30px;border-radius:5px;border:1px solid #00000060}} .lb{{font-size:10px;color:#969385;margin-top:3px}}</style>
+<h1>code samples</h1>
+<div class="wrap">{cols}</div>
+<h1>color &rarr; tree-sitter category assignment &mdash; click a header to sort</h1>
+<table class="leg" id="legtable"><thead><tr><th></th><th onclick="srt(1)">color &#9651;</th><th onclick="srt(2)">hex &#9651;</th><th onclick="srt(3)">category &#9651;</th><th>example</th></tr></thead><tbody>{legend}</tbody></table>
+<h1>palette</h1>
+{palette}\n<script>let D={{}};function srt(c){{const t=document.querySelector("#legtable tbody");const r=[...t.rows];D[c]=!D[c];r.sort((a,b)=>{{const x=a.cells[c].innerText.trim().toLowerCase(),y=b.cells[c].innerText.trim().toLowerCase();return (x<y?-1:x>y?1:0)*(D[c]?1:-1)}});r.forEach(x=>t.appendChild(x))}}</script>'''
+open("/tmp/dupre-canon.html","w").write(html)
+print("wrote /tmp/dupre-canon.html")
diff --git a/scripts/theme-selector/theme-selector.html b/scripts/theme-selector/theme-selector.html
new file mode 100644
index 00000000..827529c5
--- /dev/null
+++ b/scripts/theme-selector/theme-selector.html
@@ -0,0 +1,160 @@
+<!doctype html><meta charset=utf-8><title>theme-selector</title>
+<style>
+ body{background:#0d0b0a;color:#cdced1;font:15px/1.55 monospace;margin:20px}
+ h1{font-size:22px;font-weight:normal;color:#e8bd30;margin:26px 0 10px;border-bottom:1px solid #252321;padding-bottom:6px}
+ h2{font-size:10pt;color:#8a9496;font-weight:normal;margin:0 0 4px}
+ .wrap{display:flex;flex-wrap:nowrap;overflow-x:auto;gap:14px;padding-bottom:10px}
+ .col{flex:0 0 auto;width:460px}
+ pre{background:#0d0b0a;border:1px solid #252321;border-radius:8px;padding:14px 16px;font-size:19px;overflow:auto;white-space:pre}
+ table.leg{border-collapse:collapse} table.leg td{padding:4px 12px;vertical-align:middle}
+ 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}
+ .cat{color:#b4b1a2} .ex{font-size:17px}
+ .sbtn{width:26px;height:24px;border:1px solid #3a3a3a;border-radius:3px;background:#eaeaea;color:#111;cursor:pointer;font-size:15px;margin-right:2px;padding:0}
+ .sbtn.on{background:#0d0b0a;color:#cdced1;border-color:#8a9496}
+ .pals{display:flex;gap:8px;flex-wrap:wrap}
+ .pchip{width:128px;height:58px;border-radius:6px;border:1px solid #00000060;position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:grab}
+ .pchip.drag{opacity:.4} .pchip input.nm{background:transparent;border:none;text-align:center;font:bold 10pt monospace;width:108px;outline:none}
+ .pchip .hx{font-size:10pt;opacity:.8} .pchip .rm{position:absolute;top:2px;right:5px;background:none;border:none;cursor:pointer;font-size:14px;font-weight:bold;opacity:.7}
+ .palctl{margin-top:12px;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
+ .palctl input[type=text]{background:#161412;border:1px solid #252321;color:#cdced1;border-radius:4px;padding:5px 8px;font:10pt monospace}
+ .palctl input[type=text]::placeholder{color:#b4b1a2;opacity:1}
+ .palctl input[type=color]{width:128px;height:58px;border:1px solid #00000060;border-radius:6px;padding:2px;cursor:pointer}
+ .palctl button,.filebar button,.fbtn{background:#252321;color:#e8bd30;border:1px solid #3a3a3a;border-radius:4px;padding:6px 12px;font:10pt monospace;cursor:pointer}
+ #export{width:100%;height:180px;margin-top:10px;background:#0d0b0a;color:#a4ac64;border:1px solid #252321;border-radius:6px;font:10pt monospace;padding:10px}
+ .filebar{margin:6px 0 0;display:flex;gap:8px;align-items:center}
+ #pagetitle{font-size:30px;color:#cdced1;font-weight:normal;border:none;margin:4px 0 18px;padding:0}
+</style>
+<h1 id="pagetitle">Untitled: color palette</h1>
+<h1>code samples</h1>
+<div class="wrap"><div class="col"><h2>Elisp</h2><pre id="code-Elisp"></pre></div><div class="col"><h2>Go</h2><pre id="code-Go"></pre></div><div class="col"><h2>Python</h2><pre id="code-Python"></pre></div><div class="col"><h2>TypeScript</h2><pre id="code-TypeScript"></pre></div><div class="col"><h2>Shell</h2><pre id="code-Shell"></pre></div><div class="col"><h2>C/C++</h2><pre id="code-CC"></pre></div></div>
+<h1>color &rarr; category — chip reassigns · N/B/I sets weight &amp; slant · click a header to sort</h1>
+<table class="leg" id="legtable"><thead><tr><th onclick="srt(0)">color &#9651;</th><th>style</th><th onclick="srt(1)">category &#9651;</th><th>example</th></tr></thead><tbody id="legbody"></tbody></table>
+<h1>UI / interface faces — foreground &amp; background per face</h1>
+<table class="leg" id="uitable"><thead><tr><th>face</th><th>foreground</th><th>background</th><th>preview</th></tr></thead><tbody id="uibody"></tbody></table>
+<h1>palette — add / remove / rename / drag to reorder</h1>
+<div class="pals" id="pals"></div>
+<div class="palctl">
+ <input type="color" id="newhex" value="#888888" oninput="syncHex('swatch')">
+ <input type="text" id="newhexstr" placeholder="#rrggbb" value="#888888" oninput="syncHex('text')" style="width:110px">
+ <input type="text" id="newname" placeholder="name">
+ <button onclick="addColor()">+ add color</button>
+</div>
+<h1>save / load theme</h1>
+<div class="filebar">
+ <label style="color:#b4b1a2">theme name</label><input type="text" id="themename" value="" placeholder="untitled" oninput="updateTitle()" style="background:#161412;border:1px solid #252321;color:#cdced1;border-radius:4px;padding:5px 8px;font:10pt monospace;width:200px">
+</div>
+<div class="filebar">
+ <button onclick="download()">&#11015; download &lt;name&gt;.json</button>
+ <label class="fbtn">&#11014; load theme.json<input type="file" accept=".json" onchange="importFile(event)" style="display:none"></label>
+ <button id="jsonbtn" onclick="toggleJSON()">show JSON</button>
+</div>
+<textarea id="export" style="display:none" readonly></textarea>
+<script>
+const SAMPLES={"Elisp": [[["cmd", ";;"], ["cm", " cache.el"]], [["punc", "("], ["kw", "require"], ["p", " "], ["con", "'cl-lib"], ["punc", ")"]], [], [["punc", "("], ["kw", "defvar"], ["p", " "], ["var", "cache--tbl"], ["p", " "], ["punc", "("], ["fnc", "make-hash-table"], ["p", " "], ["con", ":test"], ["p", " "], ["con", "'equal"], ["punc", "))"]], [["p", " "], ["doc", "\"Memo table.\")"]], [], [["punc", "("], ["kw", "defun"], ["p", " "], ["fnd", "cache-get"], ["p", " "], ["punc", "("], ["var", "key"], ["punc", ")"]], [["p", " "], ["doc", "\"Return cached value for KEY.\""]], [["p", " "], ["punc", "("], ["kw", "or"], ["p", " "], ["punc", "("], ["fnc", "gethash"], ["p", " "], ["var", "key"], ["p", " "], ["var", "cache--tbl"], ["punc", ")"]], [["p", " "], ["punc", "("], ["kw", "let"], ["p", " "], ["punc", "(("], ["var", "v"], ["p", " "], ["punc", "("], ["fnc", "compute"], ["p", " "], ["var", "key"], ["p", " "], ["num", "42"], ["punc", "))) "]], [["p", " "], ["punc", "("], ["fnc", "puthash"], ["p", " "], ["var", "key"], ["p", " "], ["var", "v"], ["p", " "], ["var", "cache--tbl"], ["punc", ") "], ["var", "v"], ["punc", "))))"]]], "Go": [[["cmd", "//"], ["cm", " queue.go"]], [["kw", "package"], ["p", " "], ["var", "main"]], [], [["kw", "import"], ["p", " "], ["str", "\"fmt\""]], [], [["kw", "const"], ["p", " "], ["con", "MaxItems"], ["p", " "], ["op", "="], ["p", " "], ["num", "100"]], [], [["kw", "type"], ["p", " "], ["ty", "Order"], ["p", " "], ["kw", "struct"], ["p", " "], ["punc", "{"]], [["p", " "], ["prop", "ID"], ["p", " "], ["ty", "int"]], [["p", " "], ["prop", "Name"], ["p", " "], ["ty", "string"]], [["punc", "}"]], [], [["kw", "func"], ["p", " "], ["punc", "("], ["var", "q"], ["p", " "], ["op", "*"], ["ty", "Queue"], ["punc", ")"], ["p", " "], ["fnd", "Push"], ["punc", "("], ["var", "o"], ["p", " "], ["op", "*"], ["ty", "Order"], ["punc", ")"], ["p", " "], ["ty", "error"], ["p", " "], ["punc", "{"]], [["p", " "], ["cmd", "//"], ["cm", " reject nil"]], [["p", " "], ["kw", "if"], ["p", " "], ["var", "o"], ["p", " "], ["op", "=="], ["p", " "], ["con", "nil"], ["p", " "], ["punc", "{"]], [["p", " "], ["kw", "return"], ["p", " "], ["fnc", "fmt.Errorf"], ["punc", "("], ["str", "\"nil\""], ["punc", ")"]], [["p", " "], ["punc", "}"]], [["p", " "], ["var", "q"], ["op", "."], ["prop", "items"], ["p", " "], ["op", "="], ["p", " "], ["fnc", "append"], ["punc", "("], ["var", "q"], ["op", "."], ["prop", "items"], ["punc", ","], ["p", " "], ["var", "o"], ["punc", ")"]], [["p", " "], ["kw", "return"], ["p", " "], ["con", "nil"]], [["punc", "}"]]], "Python": [[["cmd", "#"], ["cm", " theme.py"]], [["kw", "from"], ["p", " "], ["var", "dataclasses"], ["p", " "], ["kw", "import"], ["p", " "], ["var", "dataclass"], ["punc", ","], ["p", " "], ["var", "field"]], [], [["con", "DEFAULT_PORT"], ["op", ":"], ["p", " "], ["ty", "int"], ["p", " "], ["op", "="], ["p", " "], ["num", "8080"]], [], [["dec", "@dataclass"]], [["kw", "class"], ["p", " "], ["ty", "Theme"], ["op", ":"]], [["p", " "], ["doc", "\"\"\"A color theme.\"\"\""]], [["p", " "], ["prop", "name"], ["op", ":"], ["p", " "], ["ty", "str"], ["p", " "], ["op", "="], ["p", " "], ["str", "\"dupre\""]], [["p", " "], ["prop", "colors"], ["op", ":"], ["p", " "], ["ty", "dict"], ["p", " "], ["op", "="], ["p", " "], ["fnc", "field"], ["punc", "("], ["prop", "default_factory"], ["op", "="], ["ty", "dict"], ["punc", ")"]], [], [["p", " "], ["kw", "def"], ["p", " "], ["fnd", "resolve"], ["punc", "("], ["var", "self"], ["punc", ","], ["p", " "], ["var", "key"], ["op", ":"], ["p", " "], ["ty", "str"], ["punc", ")"], ["p", " "], ["op", "->"], ["p", " "], ["ty", "str"], ["p", " "], ["op", "|"], ["p", " "], ["con", "None"], ["op", ":"]], [["p", " "], ["cmd", "#"], ["cm", " fallback to none"]], [["p", " "], ["var", "v"], ["p", " "], ["op", "="], ["p", " "], ["var", "self"], ["op", "."], ["prop", "colors"], ["op", "."], ["fnc", "get"], ["punc", "("], ["var", "key"], ["punc", ","], ["p", " "], ["str", "\""], ["esc", "\\t"], ["str", "none\""], ["punc", ")"]], [["p", " "], ["kw", "if"], ["p", " "], ["bi", "len"], ["punc", "("], ["var", "v"], ["punc", ")"], ["p", " "], ["op", "=="], ["p", " "], ["num", "0"], ["op", ":"], ["p", " "], ["kw", "return"], ["p", " "], ["con", "None"]], [["p", " "], ["kw", "return"], ["p", " "], ["var", "v"]]], "TypeScript": [[["cmd", "//"], ["cm", " orders.ts"]], [["kw", "import"], ["p", " "], ["punc", "{"], ["p", " "], ["ty", "Order"], ["p", " "], ["punc", "}"], ["p", " "], ["kw", "from"], ["p", " "], ["str", "\"./types\""]], [], [["kw", "export"], ["p", " "], ["kw", "interface"], ["p", " "], ["ty", "Queue"], ["p", " "], ["punc", "{"]], [["p", " "], ["prop", "max"], ["op", ":"], ["p", " "], ["ty", "number"], ["punc", ";"]], [["p", " "], ["prop", "items"], ["op", ":"], ["p", " "], ["ty", "Order"], ["punc", "[];"]], [["punc", "}"]], [], [["dec", "@Injectable"], ["punc", "()"]], [["kw", "export"], ["p", " "], ["kw", "class"], ["p", " "], ["ty", "OrderQueue"], ["p", " "], ["kw", "implements"], ["p", " "], ["ty", "Queue"], ["p", " "], ["punc", "{"]], [["p", " "], ["kw", "private"], ["p", " "], ["prop", "re"], ["p", " "], ["op", "="], ["p", " "], ["re", "/^#[0-9a-f]{6}$/i"], ["punc", ";"]], [], [["p", " "], ["fnd", "push"], ["punc", "("], ["var", "o"], ["op", ":"], ["p", " "], ["ty", "Order"], ["punc", ")"], ["op", ":"], ["p", " "], ["ty", "boolean"], ["p", " "], ["punc", "{"]], [["p", " "], ["kw", "if"], ["p", " "], ["punc", "("], ["var", "o"], ["p", " "], ["op", "==="], ["p", " "], ["con", "null"], ["punc", ")"], ["p", " "], ["kw", "return"], ["p", " "], ["con", "false"], ["punc", ";"]], [["p", " "], ["var", "console"], ["op", "."], ["fnc", "log"], ["punc", "("], ["str", "`id "], ["punc", "${"], ["var", "o"], ["op", "."], ["prop", "id"], ["punc", "}"], ["esc", "\\n"], ["str", "`"], ["punc", ");"]], [["p", " "], ["kw", "return"], ["p", " "], ["con", "true"], ["punc", ";"]], [["p", " "], ["punc", "}"]], [["punc", "}"]]], "Shell": [[["cmd", "#!"], ["cm", "/bin/bash"]], [["cmd", "#"], ["cm", " deploy.sh"]], [["bi", "set"], ["p", " "], ["op", "-"], ["var", "euo"], ["p", " "], ["var", "pipefail"]], [], [["var", "PORT"], ["op", "="], ["num", "8080"]], [["var", "NAME"], ["op", "="], ["str", "\"dupre\""]], [], [["fnd", "deploy"], ["punc", "()"], ["p", " "], ["punc", "{"]], [["p", " "], ["kw", "local"], ["p", " "], ["var", "target"], ["op", "="], ["str", "\"$1\""]], [["p", " "], ["kw", "if"], ["p", " "], ["punc", "[["], ["p", " "], ["op", "-z"], ["p", " "], ["str", "\"$target\""], ["p", " "], ["punc", "]]"], ["punc", ";"], ["p", " "], ["kw", "then"]], [["p", " "], ["bi", "echo"], ["p", " "], ["str", "\"no target\""]], [["p", " "], ["kw", "return"], ["p", " "], ["num", "1"]], [["p", " "], ["kw", "fi"]], [["p", " "], ["fnc", "rsync"], ["p", " "], ["op", "-az"], ["p", " "], ["str", "\"$NAME\""], ["p", " "], ["str", "\"$target\""]], [["punc", "}"]]], "C/C++": [[["cmd", "//"], ["cm", " theme.c"]], [["pp", "#include"], ["p", " "], ["str", "<stdio.h>"]], [["pp", "#define"], ["p", " "], ["con", "MAX_PORT"], ["p", " "], ["num", "8080"]], [], [["kw", "typedef"], ["p", " "], ["kw", "struct"], ["p", " "], ["punc", "{"]], [["p", " "], ["ty", "int"], ["p", " "], ["prop", "id"], ["punc", ";"]], [["p", " "], ["ty", "char"], ["p", " "], ["op", "*"], ["prop", "name"], ["punc", ";"]], [["punc", "}"], ["p", " "], ["ty", "Order"], ["punc", ";"]], [], [["ty", "int"], ["p", " "], ["fnd", "push"], ["punc", "("], ["ty", "Order"], ["p", " "], ["op", "*"], ["var", "o"], ["punc", ")"], ["p", " "], ["punc", "{"]], [["p", " "], ["kw", "if"], ["p", " "], ["punc", "("], ["var", "o"], ["p", " "], ["op", "=="], ["p", " "], ["con", "NULL"], ["punc", ")"], ["p", " "], ["punc", "{"]], [["p", " "], ["kw", "return"], ["p", " "], ["num", "-1"], ["punc", ";"]], [["p", " "], ["punc", "}"]], [["p", " "], ["fnc", "printf"], ["punc", "("], ["str", "\"id=%d"], ["esc", "\\n"], ["str", "\""], ["punc", ","], ["p", " "], ["var", "o"], ["op", "->"], ["prop", "id"], ["punc", ");"]], [["p", " "], ["kw", "return"], ["p", " "], ["num", "0"], ["punc", ";"]], [["punc", "}"]]]}, CATS=[["bg", "background (ground)", "Aa Bb 123"], ["p", "fg \u00b7 default text", "other / whitespace"], ["kw", "keyword", "class def if return"], ["bi", "builtin", "len echo printf"], ["pp", "preprocessor", "#include #define"], ["fnd", "function \u00b7 def", "resolve push"], ["fnc", "function \u00b7 call", "printf rsync get"], ["dec", "decorator", "@dataclass"], ["ty", "type / class", "int str Order Queue"], ["prop", "property / field", "id name items"], ["con", "constant", "None nil NULL true"], ["num", "number", "8080 100 -1"], ["str", "string", "\"dupre\" \"fmt\""], ["esc", "escape", "\\n \\t"], ["re", "regexp", "/^#[0-9a-f]+/"], ["doc", "docstring", "\"\"\"...\"\"\""], ["cm", "comment", "# reject nil"], ["cmd", "comment delim", "# // ;;"], ["var", "variable / use", "value key self"], ["op", "operator", ": = -> =="], ["punc", "punctuation", "{ } ( ) ;"]], UI_FACES=[["cursor", "cursor", "Aa|"], ["region", "region (selection)", "selected text"], ["hl-line", "hl-line (current line)", "current line"], ["highlight", "highlight", "hover"], ["mode-line", "mode-line", "status active"], ["mode-line-inactive", "mode-line-inactive", "status idle"], ["fringe", "fringe", "| |"], ["line-number", "line-number", " 42"], ["line-number-current-line", "line-number-current-line", "> 42"], ["minibuffer-prompt", "minibuffer-prompt", "M-x "], ["isearch", "isearch (match)", "match"], ["lazy-highlight", "lazy-highlight", "other match"], ["isearch-fail", "isearch-fail", "no match"], ["show-paren-match", "show-paren-match", "( )"], ["show-paren-mismatch", "show-paren-mismatch", ") ("], ["link", "link", "https://"], ["error", "error", "error!"], ["warning", "warning", "warning"], ["success", "success", "ok"], ["vertical-border", "vertical-border", "|"]];
+let MAP={"kw": "#67809c", "bi": "#67809c", "pp": "#67809c", "fnd": "#a9b2bb", "fnc": "#a9b2bb", "dec": "#e8bd30", "ty": "#9b5fd0", "prop": "#838d97", "con": "#cb6b4d", "num": "#cb6b4d", "esc": "#cb6b4d", "str": "#5d9b86", "re": "#5d9b86", "doc": "#5d9b86", "cm": "#be9e74", "cmd": "#a9b2bb", "var": "#e8bd30", "op": "#a9b2bb", "punc": "#a9b2bb", "p": "#cdced1", "bg": "#0d0b0a"}, PALETTE=[["#67809c", "blue"], ["#e8bd30", "gold"], ["#9b5fd0", "regal"], ["#2ba178", "emerald"], ["#5d9b86", "sage"], ["#cb6b4d", "terracotta"], ["#be9e74", "tan"], ["#cdced1", "white"], ["#a9b2bb", "silver"], ["#838d97", "steel"], ["#5e6770", "pewter"], ["#2f343a", "gunmetal"], ["#264364", "navy"], ["#0d0b0a", "ground"], ["#1a1714", "bg-dim"]], BOLD={"kw": true, "bi": false, "pp": false, "fnd": true, "fnc": false, "dec": false, "ty": false, "prop": false, "con": false, "num": false, "esc": false, "str": false, "re": false, "doc": false, "cm": false, "cmd": false, "var": false, "op": false, "punc": false, "p": false}, ITALIC={}, UIMAP={"cursor": {"fg": null, "bg": "#a9b2bb"}, "region": {"fg": null, "bg": "#264364"}, "hl-line": {"fg": null, "bg": "#1a1714"}, "highlight": {"fg": null, "bg": "#2f343a"}, "mode-line": {"fg": "#cdced1", "bg": "#2f343a"}, "mode-line-inactive": {"fg": "#838d97", "bg": "#1a1714"}, "fringe": {"fg": null, "bg": "#0d0b0a"}, "line-number": {"fg": "#5e6770", "bg": null}, "line-number-current-line": {"fg": "#e8bd30", "bg": "#1a1714"}, "minibuffer-prompt": {"fg": "#67809c", "bg": null}, "isearch": {"fg": "#0d0b0a", "bg": "#e8bd30"}, "lazy-highlight": {"fg": "#0d0b0a", "bg": "#838d97"}, "isearch-fail": {"fg": "#cb6b4d", "bg": null}, "show-paren-match": {"fg": null, "bg": "#264364"}, "show-paren-mismatch": {"fg": "#0d0b0a", "bg": "#cb6b4d"}, "link": {"fg": "#67809c", "bg": null}, "error": {"fg": "#cb6b4d", "bg": null}, "warning": {"fg": "#e8bd30", "bg": null}, "success": {"fg": "#5d9b86", "bg": null}, "vertical-border": {"fg": "#2f343a", "bg": null}};
+function esc(t){return t.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
+function lin(c){c/=255;return c<=0.03928?c/12.92:Math.pow((c+0.055)/1.055,2.4);}
+function rl(h){return 0.2126*lin(parseInt(h.substr(1,2),16))+0.7152*lin(parseInt(h.substr(3,2),16))+0.0722*lin(parseInt(h.substr(5,2),16));}
+function textOn(h){const L=rl(h);return ((L+0.05)/0.05)>(1.05/(L+0.05))?'#000':'#fff';}
+function cid(l){return l.replace(/\W/g,'');}
+function renderCode(){
+ for(const lang in SAMPLES){let html='';
+ for(const line of SAMPLES[lang]){
+ if(line.length===0){html+='\n';continue;}
+ for(const [k,t] of line){const c=MAP[k]||'#cdced1';const w=BOLD[k]?'bold':'normal';const s=ITALIC[k]?'italic':'normal';
+ html+=`<span style="color:${c};font-weight:${w};font-style:${s}">${esc(t)}</span>`;}
+ html+='\n';}
+ document.getElementById('code-'+cid(lang)).innerHTML=html;}
+}
+function buildTable(){
+ const tb=document.getElementById('legbody');tb.innerHTML='';
+ for(const [kind,label,ex] of CATS){
+ const tr=document.createElement('tr');
+ const sel=document.createElement('select');sel.className='chip';
+ const cur=MAP[kind];const have=PALETTE.some(p=>p[0]===cur);
+ const list=have?PALETTE:[[cur,'(gone) '+cur],...PALETTE];
+ for(const [hex,name] of list){const o=document.createElement('option');o.value=hex;o.textContent=name+' '+hex;o.style.background=hex;o.style.color=textOn(hex);sel.appendChild(o);}
+ sel.value=cur;
+ const exTd=document.createElement('td');exTd.className='ex';exTd.textContent=ex;
+ function styleChip(){sel.style.background=sel.value;sel.style.color=textOn(sel.value);}
+ function styleEx(){exTd.style.color=(kind==='bg'?MAP['p']:MAP[kind]);exTd.style.background=MAP['bg'];exTd.style.fontWeight=BOLD[kind]?'bold':'normal';exTd.style.fontStyle=ITALIC[kind]?'italic':'normal';}
+ styleChip();styleEx();
+ sel.onchange=()=>{MAP[kind]=sel.value;styleChip();styleEx();renderCode();if(kind==='bg')applyGround();};
+ // style buttons
+ const stTd=document.createElement('td');
+ if(kind!=='bg'){const defs=[['N','a','normal'],['B','a','bold'],['I','a','italic']];
+ const btns={};
+ defs.forEach(([id,ch,mode])=>{const b=document.createElement('button');b.className='sbtn';b.style.fontWeight=mode==='bold'?'bold':'normal';b.style.fontStyle=mode==='italic'?'italic':'normal';b.textContent=ch;
+ b.onclick=()=>{if(mode==='normal'){BOLD[kind]=false;ITALIC[kind]=false;}else if(mode==='bold'){BOLD[kind]=!BOLD[kind];}else{ITALIC[kind]=!ITALIC[kind];}refresh();renderCode();styleEx();};
+ btns[mode]=b;stTd.appendChild(b);});
+ function refresh(){btns.normal.classList.toggle('on',!BOLD[kind]&&!ITALIC[kind]);btns.bold.classList.toggle('on',!!BOLD[kind]);btns.italic.classList.toggle('on',!!ITALIC[kind]);}
+ refresh();}
+ const c0=document.createElement('td');c0.appendChild(sel);
+ const c2=document.createElement('td');c2.className='cat';c2.textContent=label;
+ tr.appendChild(c0);tr.appendChild(stTd);tr.appendChild(c2);tr.appendChild(exTd);
+ tb.appendChild(tr);}
+}
+let dragFrom=null;
+function renderPalette(){
+ const p=document.getElementById('pals');p.innerHTML='';
+ PALETTE.forEach((pc,i)=>{const [hex,name]=pc;const tc=textOn(hex);
+ const d=document.createElement('div');d.className='pchip';d.style.background=hex;d.draggable=true;
+ d.innerHTML=`<button class="rm" title="remove" style="color:${tc}">×</button><input class="nm" value="${name}" style="color:${tc}"><div class="hx" style="color:${tc}">${hex}</div>`;
+ d.querySelector('.rm').onclick=()=>{PALETTE.splice(i,1);renderPalette();buildTable();};
+ d.querySelector('.nm').onchange=(e)=>{PALETTE[i][1]=e.target.value;buildTable();buildUITable();};
+ d.ondragstart=()=>{dragFrom=i;d.classList.add('drag');};
+ d.ondragend=()=>d.classList.remove('drag');
+ d.ondragover=(e)=>e.preventDefault();
+ d.ondrop=(e)=>{e.preventDefault();if(dragFrom===null||dragFrom===i)return;const m=PALETTE.splice(dragFrom,1)[0];PALETTE.splice(i,0,m);dragFrom=null;renderPalette();buildTable();};
+ p.appendChild(d);});
+ buildUITable();
+}
+function normHex(s){s=s.trim();if(/^[0-9a-fA-F]{6}$/.test(s))s='#'+s;return /^#[0-9a-fA-F]{6}$/.test(s)?s.toLowerCase():null;}
+function syncHex(src){const sw=document.getElementById('newhex'),tx=document.getElementById('newhexstr');
+ if(src==='swatch'){tx.value=sw.value;}else{const h=normHex(tx.value);if(h)sw.value=h;}}
+function addColor(){const h=normHex(document.getElementById('newhexstr').value)||document.getElementById('newhex').value;const name=document.getElementById('newname').value||h;PALETTE.push([h,name]);document.getElementById('newname').value='';renderPalette();buildTable();}
+function themeName(){return (document.getElementById('themename').value||'theme').trim()||'theme';}
+function fileSlug(){return themeName().replace(/[^A-Za-z0-9._-]+/g,'-').replace(/^-+|-+$/g,'')||'theme';}
+function exportObj(){const a={};CATS.forEach(c=>a[c[0]]=MAP[c[0]]);return {name:themeName(),palette:PALETTE,assignments:a,bold:Object.keys(BOLD).filter(k=>BOLD[k]),italic:Object.keys(ITALIC).filter(k=>ITALIC[k]),ui:UIMAP};}
+function exportState(){const t=document.getElementById('export');t.value=JSON.stringify(exportObj(),null,1);t.style.display='block';t.focus();t.select();}
+function toggleJSON(){const t=document.getElementById('export'),b=document.getElementById('jsonbtn');if(t.style.display==='block'){t.style.display='none';b.textContent='show JSON';}else{exportState();b.textContent='hide JSON';}}
+function updateTitle(){const n=document.getElementById('themename').value.trim();document.getElementById('pagetitle').textContent=(n||'Untitled')+': color palette';}
+function download(){const blob=new Blob([JSON.stringify(exportObj(),null,1)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=fileSlug()+'.json';a.click();}
+function importFile(ev){const f=ev.target.files[0];if(!f)return;const r=new FileReader();
+ r.onload=()=>{try{const d=JSON.parse(r.result);if(d.name)document.getElementById('themename').value=d.name;if(d.palette)PALETTE=d.palette;if(d.assignments)Object.assign(MAP,d.assignments);
+ BOLD={};(d.bold||[]).forEach(k=>BOLD[k]=true);ITALIC={};(d.italic||[]).forEach(k=>ITALIC[k]=true);
+ if(d.ui)Object.assign(UIMAP,d.ui);
+ renderPalette();buildTable();buildUITable();renderCode();applyGround();updateTitle();}catch(e){alert('bad theme file: '+e.message);}};
+ r.readAsText(f);ev.target.value='';}
+function applyGround(){document.querySelectorAll('pre').forEach(p=>p.style.background=MAP['bg']);document.querySelectorAll('.ex').forEach(e=>e.style.background=MAP['bg']);}
+function uiSelect(face,attr){
+ const sel=document.createElement('select');sel.className='chip';
+ const none=document.createElement('option');none.value='';none.textContent='— none —';none.style.background='#161412';none.style.color='#b4b1a2';sel.appendChild(none);
+ for(const [hex,name] of PALETTE){const o=document.createElement('option');o.value=hex;o.textContent=name+' '+hex;o.style.background=hex;o.style.color=textOn(hex);sel.appendChild(o);}
+ sel.value=UIMAP[face][attr]||'';
+ function style(){if(sel.value){sel.style.background=sel.value;sel.style.color=textOn(sel.value);}else{sel.style.background='#161412';sel.style.color='#b4b1a2';}}
+ style();
+ sel.onchange=()=>{UIMAP[face][attr]=sel.value||null;style();paintUI(face);};
+ return sel;
+}
+function paintUI(face){const pv=document.getElementById('uiprev-'+face);if(!pv)return;pv.style.color=UIMAP[face].fg||MAP['p'];pv.style.background=UIMAP[face].bg||MAP['bg'];}
+function buildUITable(){
+ const tb=document.getElementById('uibody');tb.innerHTML='';
+ for(const [face,label,ex] of UI_FACES){
+ const tr=document.createElement('tr');
+ const c0=document.createElement('td');c0.className='cat';c0.textContent=label;
+ const cF=document.createElement('td');cF.appendChild(uiSelect(face,'fg'));
+ const cB=document.createElement('td');cB.appendChild(uiSelect(face,'bg'));
+ 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(cP);tb.appendChild(tr);paintUI(face);
+ }
+}
+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[2].innerText).toLowerCase(),
+ y=(c===0?b.querySelector('select').value:b.cells[2].innerText).toLowerCase();
+ return (x<y?-1:x>y?1:0)*(D[c]?1:-1);});r.forEach(x=>tb.appendChild(x));}
+renderPalette();buildTable();buildUITable();renderCode();applyGround();updateTitle();
+</script> \ No newline at end of file