diff options
Diffstat (limited to 'scripts/theme-studio/app.js')
| -rw-r--r-- | scripts/theme-studio/app.js | 822 |
1 files changed, 252 insertions, 570 deletions
diff --git a/scripts/theme-studio/app.js b/scripts/theme-studio/app.js index a4e0da9c1..ce1480ffb 100644 --- a/scripts/theme-studio/app.js +++ b/scripts/theme-studio/app.js @@ -1,10 +1,11 @@ const SAMPLES=SAMPLES_J, CATS=CATS_J, UI_FACES=UIFACES_J, APPS=APPS_J; const COLOR_NAMES=COLOR_NAMES_J; +const FACE_DOCS=FACE_DOCS_J, SYNTAX_DOCS=SYNTAX_DOCS_J; // face/category -> docstring first line, for element hovers let MAP=MAP_J, PALETTE=PALETTE_J, SYNTAX=SYNTAX_J, UIMAP=UIMAP_J; let LOCKED=new Set(LOCKS_J); // rows whose choice is decided (controls disabled, skipped by erase/reset batch actions) const DELTAE_MIN=0.02; // OKLab ΔE below this = colors too close to tell apart (perceptual-metrics spec) const DEFAULT_UIMAP=JSON.parse(JSON.stringify(UIMAP)); -function syntaxBlank(k){return {fg:MAP[k]||null,bg:null,bold:false,italic:false,underline:false,strike:false,box:null};} +function syntaxBlank(k){return {fg:MAP[k]||null,bg:null,'distant-fg':null,family:null,weight:null,slant:null,underline:null,strike:null,overline:null,box:null,inverse:false,extend:false,inherit:null,height:null};} function syncSyntaxCache(k){const s=SYNTAX[k]||syntaxBlank(k);MAP[k]=s.fg||'';} function syncAllSyntaxCache(){CATS.forEach(c=>syncSyntaxCache(c[0]));} function syncSyntaxFromCache(){CATS.forEach(c=>{const k=c[0];syntaxFace(k).fg=MAP[k]||null;});} @@ -19,6 +20,16 @@ const DEFAULT_SYNTAX=JSON.parse(JSON.stringify(SYNTAX)); function pname(n){return nameToHex(n,PALETTE);} function seedPkgmap(){return buildPkgmap(APPS,PALETTE);} let PKGMAP=seedPkgmap(); +// Preview-locate registry (preview-locate spec). One cached, module-level +// registry rebuilt once per assignment / import / reset / view-switch batch — at +// the top of the two preview renderers (buildPkgPreview, buildMockFrame), which +// every such path funnels through before spans render. Never rebuilt per hover or +// per span. locate-onpane is recomputed from the current view at render time +// (isLocateOnPane), never stored here. Built lazily (not at declaration): the +// inlined buildLocateRegistry / UI_INHERIT from app-core.js are spliced below +// this point, so an init call here would hit the const's temporal dead zone. +let LOCATE_REG={}; +function rebuildLocateRegistry(){LOCATE_REG=buildLocateRegistry(APPS,PKGMAP,UIMAP,MAP);return LOCATE_REG;} function esc(t){return t.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');} // Pure color-math core (lin/rl/contrast/rating/hsv2rgb/rgb2hsv/hex2rgb/rgb2hex, // plus OKLab/OKLCH/APCA/deltaE), inlined verbatim from colormath.js. @@ -35,7 +46,7 @@ PALETTE_GENERATOR_UI_J // The contrast-cell readout shared by every table: a WCAG ratio colored by its // table verdict. Callers compute r for their own fg/bg. function verdictFor(r,target=4.5){return r>=target?'PASS':'FAIL';} -function crHtml(r,target=4.5){const v=verdictFor(r,target);return `<span style="color:${ratingColor(r)}">${r.toFixed(1)} ${v}</span>`;} +function crHtml(r){return `<span style="color:${ratingColor(r)}" title="${esc(contrastTitle(r))}">${r.toFixed(1)}</span>`;} // Effective fg/bg with the standard fallback: an unset foreground reads as the // default fg (MAP['p']), an unset background as the ground (MAP['bg']). All three // tiers resolve their raw value through these before measuring or rendering. @@ -46,7 +57,7 @@ function effBg(v){return v||MAP['bg'];} // fg:MAP['p']} repeated across app.js, palette-actions.js, and the browser gates. function groundPair(){return {bg:MAP['bg'],fg:MAP['p']};} function cid(l){return l.replace(/\W/g,'');} -function buildLangSel(){const s=document.getElementById('langsel');s.innerHTML='';for(const lang in SAMPLES){const o=document.createElement('option');o.value=lang;o.textContent=lang;s.appendChild(o);}} +function buildLangSel(){const s=document.getElementById('langsel');s.innerHTML='';for(const lang of Object.keys(SAMPLES).sort((a,b)=>a.localeCompare(b))){const o=document.createElement('option');o.value=lang;o.textContent=lang;s.appendChild(o);}if(SAMPLES['Elisp'])s.value='Elisp';} function renderCode(){ const lang=document.getElementById('langsel').value;let html=''; for(const line of SAMPLES[lang]){ @@ -57,95 +68,29 @@ function renderCode(){ cp.onclick=(e)=>{const s=e.target.closest('[data-k]');if(s)flashAssign(s.dataset.k);}; buildMockFrame(); } -// Custom color dropdown: a real swatch + name + hex per row, since native -// <option> background colors render unreliably on Linux Chrome. The popup is -// fixed-positioned on <body> so a table's overflow can't clip it. -let _ddPop=null; -function closeColorDropdown(){if(_ddPop){_ddPop.remove();_ddPop=null;}} -document.addEventListener('pointerdown',e=>{if(_ddPop&&!e.target.closest('.cdd')&&!e.target.closest('.cddpop'))closeColorDropdown();}); -function mkColorDropdown(options,cur,onPick,opts={}){ - const wrap=document.createElement('div');wrap.className='cstep'; - const left=document.createElement('button'),right=document.createElement('button'); - left.className='cstepbtn';right.className='cstepbtn';left.type=right.type='button'; - left.textContent='‹';right.textContent='›';left.title='move to next darker color in this column';right.title='move to next lighter color in this column'; - const t=document.createElement('div');t.className='cdd'+(opts.compact?' compact':'');t.tabIndex=0; - const nameOf=h=>{const o=options.find(p=>p[0]===h);return o?o[1]:(h||'none');}; - const displayHex=h=>h||(opts.defaultHex||''); - const displayName=h=>h?nameOf(h):(opts.defaultName||nameOf(h)); - function step(dir){if(wrap.dataset.locked==='1')return;const next=spanNeighborHex(cur,PALETTE,groundPair(),dir);if(!next)return;cur=next;paint();onPick(next);} - function paintStepButtons(){ - const locked=wrap.dataset.locked==='1'; - left.disabled=locked||!spanNeighborHex(cur,PALETTE,groundPair(),-1); - right.disabled=locked||!spanNeighborHex(cur,PALETTE,groundPair(),1); - } - function paint(){const shown=displayHex(cur),nm=displayName(cur),ttl=cur?(nm+' '+cur):(nm+(shown?' -> '+shown:''));t.style.background=shown||'#161412';t.style.color=shown?textOn(shown):'#b4b1a2';t.dataset.val=cur||'';t.title=ttl;t.classList.toggle('is-default',!cur);t.classList.toggle('gone',!!cur&&nameOf(cur)==='(gone)'); - t.innerHTML=opts.compact?`<span class="cddsw" style="background:${shown||'transparent'}"></span>`:`<span class="cddsw" style="background:${shown||'transparent'}"></span>${esc(nm)}`;paintStepButtons();} - paint(); - left.onclick=e=>{e.stopPropagation();step(-1);}; - right.onclick=e=>{e.stopPropagation();step(1);}; - t.onclick=(e)=>{e.stopPropagation();if(wrap.dataset.locked==='1')return;if(_ddPop){closeColorDropdown();return;} - // 2D gallery: a grid of swatches in the palette-panel shape (ground strip, - // then one row per family) instead of a long vertical list. galleryModel is - // the shared pure layout (app-core.js). - const pop=document.createElement('div');pop.className='cddpop cddgrid'; - const model=galleryModel(cur,PALETTE,groundPair()); - const pick=(hex)=>{cur=hex;paint();closeColorDropdown();onPick(hex);}; - const head=document.createElement('div');head.className='cddghead'; - const def=document.createElement('button');def.type='button'; - def.className='cddgdef'+(model.default.selected?' sel':''); - def.textContent=opts.defaultName||'default';def.title='clear — use the default'; - def.onclick=(ev)=>{ev.stopPropagation();pick('');};head.appendChild(def); - if(model.gone){const g=document.createElement('span');g.className='cddgc gone sel'; - g.style.background=model.gone.hex;g.title='(gone) '+model.gone.hex;head.appendChild(g); - const gl=document.createElement('span');gl.className='cddglbl';gl.textContent='(gone) '+model.gone.hex;head.appendChild(gl);} - pop.appendChild(head); - for(const row of model.rows){const rr=document.createElement('div');rr.className='cddgrow'; - for(const c of row.cells){const sw=document.createElement('button');sw.type='button'; - sw.className='cddgc'+(c.selected?' sel':'');sw.style.background=c.hex; - sw.dataset.hex=c.hex;sw.dataset.name=c.name;sw.title=c.name+' '+c.hex; - sw.onclick=(ev)=>{ev.stopPropagation();pick(c.hex);};rr.appendChild(sw);} - pop.appendChild(rr);} - document.body.appendChild(pop);const r=t.getBoundingClientRect(); - pop.style.left=r.left+'px';pop.style.minWidth=r.width+'px'; - pop.style.top=(r.bottom+2)+'px'; - const ph=pop.getBoundingClientRect().height; - if(r.bottom+ph>window.innerHeight-6)pop.style.top=Math.max(6,r.top-ph-2)+'px'; - const pr=pop.getBoundingClientRect(); - if(pr.right>window.innerWidth-6)pop.style.left=Math.max(6,window.innerWidth-6-pr.width)+'px'; - _ddPop=pop;}; - t.setValue=h=>{cur=h;paint();}; - wrap.setValue=h=>{cur=h;paint();}; - wrap.syncLocked=paintStepButtons; - wrap.appendChild(left);wrap.appendChild(t);wrap.appendChild(right);paintStepButtons(); - return wrap;} -// Standard option list for a swatch dropdown: a "default" entry, then the -// palette in the same ground/column order as the palette panel. If cur is set -// but no longer in the palette, surface it as a "(gone)" entry so the row still -// shows what it points at. Shared by all three tiers. -function ddList(cur){return paletteOptionList(cur,PALETTE,groundPair());} -// Shared lock toggle for any table row. lockKey is namespaced per tier (bare -// syntax kind, 'ui:'+face, 'pkg:'+app+':'+face). els are the row's editable -// controls — native selects/buttons/inputs are disabled; the custom swatch -// dropdown (a div) gets data-locked so its onclick refuses to open. -function mkLockCell(lockKey,els){ - const td=document.createElement('td');td.style.textAlign='center'; - const lk=document.createElement('button');lk.className='lockbtn'; - function paint(){const on=LOCKED.has(lockKey);lk.textContent=on?'🔒':'🔓';lk.classList.toggle('on',on); - lk.title=on?'locked — click to unlock':'click to lock this decision'; - (els||[]).forEach(el=>{if(!el)return; - if(el.tagName==='SELECT'||el.tagName==='BUTTON'||el.tagName==='INPUT')el.disabled=on; - else{el.dataset.locked=on?'1':'';el.classList.toggle('locked',on);if(el.syncLocked)el.syncLocked();}});} - lk.onclick=()=>{LOCKED.has(lockKey)?LOCKED.delete(lockKey):LOCKED.add(lockKey);paint();updateLockToggles();}; - paint();td.appendChild(lk);return td;} -// B/I/U/S style buttons shared by the UI and package tables. isOn(attr) reads the -// current state of an attribute, onToggle(attr) flips it and repaints. Returns -// the button list so the caller appends them and hands them to mkLockCell. -function mkStyleButtons(isOn,onToggle){ - return ['bold','italic','underline','strike'].map(at=>{ - const b=document.createElement('button');b.className='sbtn'+(isOn(at)?' on':'');b.textContent='a'; - b.style.fontWeight=at==='bold'?'bold':'normal';b.style.fontStyle=at==='italic'?'italic':'normal'; - b.style.textDecoration=at==='underline'?'underline':at==='strike'?'line-through':'none';b.title=at; - b.onclick=()=>{onToggle(at);b.classList.toggle('on',!!isOn(at));};return b;});} +CONTROLS_J +// Expand/collapse every row in a table at once, then sync the per-row triangles. +function setAllExpanded(tableId,expand){ + const tb=document.getElementById(tableId);if(!tb)return; + tb.querySelectorAll('tr.detailrow').forEach(d=>{d.style.display=expand?'':'none';const k=d.dataset.detailFor;if(k){expand?EXPANDED.add(k):EXPANDED.delete(k);}}); + tb.querySelectorAll('.exptoggle').forEach(b=>{b.textContent=expand?'▼':'▶';b.classList.toggle('on',expand);}); +} +// The header-level expand/collapse-all toggle for a table. Its label and triangle +// track the aggregate: any row open -> ▼ collapse all; all closed -> ▶ expand all. +const EXPALL_TABLE={syntaxexpandall:'legbody',uiexpandall:'uibody',pkgexpandall:'pkgbody'}; +function syncExpandAllBtns(){ + for(const id in EXPALL_TABLE){const btn=document.getElementById(id);const tb=document.getElementById(EXPALL_TABLE[id]);if(!btn||!tb)continue; + const anyOpen=[...tb.querySelectorAll('tr.detailrow')].some(d=>d.style.display!=='none'); + btn.textContent=anyOpen?'▼ collapse all':'▶ expand all';} +} +function toggleAllExpanded(id){ + const tableId=EXPALL_TABLE[id],tb=document.getElementById(tableId);if(!tb)return; + const anyOpen=[...tb.querySelectorAll('tr.detailrow')].some(d=>d.style.display!=='none'); + setAllExpanded(tableId,!anyOpen);syncExpandAllBtns(); +} +// Column count for a table's detail-row colspan, read from its header so the +// expander never hardcodes a width that drifts when a column is added. +function tableColCount(tableId){const h=document.querySelector('#'+tableId+' thead tr');return h?h.cells.length:1;} // Apply a batch action to every editable row in a tier. keyFn maps a row entry to // its lock key, or null to skip the row entirely (syntax bg and the default fg); // resetFn does the actual clearing. Locked rows are left untouched. @@ -153,7 +98,7 @@ function clearUnlockedRows(items,keyFn,resetFn){ for(const it of items){const k=keyFn(it);if(k===null)continue;if(!LOCKED.has(k))resetFn(it);} } function rebuildColorTables(){ - buildTable();buildUITable();if(document.getElementById('pkgbody'))buildPkgTable(); + buildTable();buildUITable();buildPkgTable();// buildPkgTable self-guards when #pkgbody is absent } function refreshPaletteState(opts={}){ renderPalette();rebuildColorTables(); @@ -170,7 +115,7 @@ function updateLockToggle(tier){ const ids={syntax:'syntaxlocktoggle',ui:'uilocktoggle',pkg:'pkglocktoggle'},b=document.getElementById(ids[tier]);if(!b)return; b.textContent=lockToggleLabel(tierLockKeys(tier),LOCKED); } -function updateLockToggles(){updateLockToggle('syntax');updateLockToggle('ui');updateLockToggle('pkg');} +function updateLockToggles(){updateLockToggle('syntax');updateLockToggle('ui');updateLockToggle('pkg');updateViewLockIndicators();} function toggleAllLocks(tier){ const all=areAllLocked(tierLockKeys(tier),LOCKED); LOCKED=toggleLockSet(tierLockKeys(tier),LOCKED); @@ -209,22 +154,25 @@ function buildTable(){ const crTd=document.createElement('td');crTd.style.whiteSpace='nowrap';crTd.style.fontSize='10pt'; function rowFg(){return kind==='bg'?MAP['p']:effFg(syntaxFace(kind).fg);} function rowBg(){return syntaxFace(kind).bg||MAP['bg'];} - function styleEx(){const s=syntaxFace(kind);exTd.style.color=rowFg();exTd.style.background=rowBg();exTd.style.fontWeight=s.bold?'bold':'normal';exTd.style.fontStyle=s.italic?'italic':'normal';exTd.style.textDecoration=(s.underline?'underline ':'')+(s.strike?'line-through':'')||'none';exTd.style.boxShadow=boxCss(s.box,rowBg());} + function styleEx(){const s=syntaxFace(kind);exTd.style.color=rowFg();exTd.style.background=rowBg();exTd.style.fontWeight=cssWeight(s.weight);exTd.style.fontStyle=s.slant||'normal';exTd.style.textDecoration=(s.underline?'underline ':'')+(s.strike?'line-through':'')||'none';exTd.style.boxShadow=boxCss(s.box,rowBg());} function styleCr(){const r=contrast(rowFg(),rowBg());crTd.innerHTML=crHtml(r);} const dd=mkColorDropdown(list,cur,(hex)=>{const s=syntaxFace(kind);s.fg=hex||null;syncSyntaxCache(kind);styleEx();styleCr();renderCode();if(kind==='bg'||kind==='p'){applyGround();buildTable();buildPkgTable();buildPkgPreview();}repaintCovered();},{compact:true,defaultHex:rowFg()}); const bgd=mkColorDropdown(ddList(sf.bg||''),sf.bg||'',hex=>{const s=syntaxFace(kind);s.bg=hex||null;styleEx();styleCr();renderCode();repaintCovered();},{compact:true,defaultHex:rowBg()}); styleEx();styleCr(); const stTd=document.createElement('td'); - const stBtns=mkStyleButtons(at=>syntaxFace(kind)[at],at=>{const s=syntaxFace(kind);s[at]=!s[at];styleEx();renderCode();}); - const stCluster=document.createElement('div');stCluster.className='stylecluster';stBtns.forEach(b=>stCluster.appendChild(b));stTd.appendChild(stCluster); + const stCtls=mkStyleControls(syntaxFace(kind),()=>{styleEx();renderCode();},{defaultHex:rowFg()}); + const stCluster=document.createElement('div');stCluster.className='stylecluster';stCtls.forEach(c=>stCluster.appendChild(c));stTd.appendChild(stCluster); const c0=document.createElement('td');c0.appendChild(dd); const cB=document.createElement('td');cB.appendChild(bgd); const cX=document.createElement('td');const boxCtl=mkBoxControl(()=>syntaxFace(kind).box,b=>{syntaxFace(kind).box=b;styleEx();renderCode();},{compact:true});cX.appendChild(boxCtl); - const lkTd=mkLockCell(kind,[dd,bgd,...stBtns,boxCtl]); - const c2=document.createElement('td');c2.className='cat';c2.textContent=label;c2.style.cursor='pointer';c2.title='flash this category in the code';c2.onclick=()=>flashTokens(kind); - tr.appendChild(c2);tr.appendChild(lkTd);tr.appendChild(c0);tr.appendChild(cB);tr.appendChild(stTd);tr.appendChild(cX);tr.appendChild(crTd);tr.appendChild(exTd); - tb.appendChild(tr);} - updateLockToggle('syntax'); + const exp=mkExpander(syntaxFace(kind),tableColCount('legtable'),()=>{styleEx();renderCode();},{expandKey:kind,showInheritHeight:true,inheritOptions:[''].concat(BASE_INHERITS),defaultHex:rowFg(),ndCheck:()=>overflowNonDefault(syntaxFace(kind),DEFAULT_SYNTAX[kind],true)}); + exp.detail.dataset.detailFor=kind; + const lkTd=mkLockCell(kind,[dd,bgd,...stCtls,boxCtl,...exp.locks]); + const c2=document.createElement('td');c2.className='cat';c2.title=composeHoverTitle(SYNTAX_DOCS[kind],c2.title);c2.appendChild(exp.btn); + const c2lbl=document.createElement('span');c2lbl.textContent=' '+label;c2lbl.style.cursor='pointer';c2lbl.title='flash this category in the code';c2lbl.onclick=()=>flashTokens(kind);c2.appendChild(c2lbl); + tr.appendChild(lkTd);tr.appendChild(c2);tr.appendChild(c0);tr.appendChild(cB);tr.appendChild(stTd);tr.appendChild(cX);tr.appendChild(crTd);tr.appendChild(exTd); + tb.appendChild(tr);tb.appendChild(exp.detail);} + updateLockToggle('syntax');syncExpandAllBtns(); } PALETTE_ACTIONS_J function notify(msg,err){const m=document.getElementById('palmsg');if(!m)return;m.textContent=msg;m.style.color=err?'#cb6b4d':'#8a9496';m.style.opacity='1';clearTimeout(m._t);m._t=setTimeout(()=>{m.style.opacity='0';},err?4000:2800);} @@ -358,12 +306,23 @@ function exportObj(){normalizePalette();const o={name:themeName(),palette:PALETT 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';}else{exportState();b.textContent='hide';}} function updateTitle(){const n=document.getElementById('themename').value.trim();document.getElementById('pagetitle').textContent=(n||'Untitled')+': theme';} -function exportTheme(){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();} +// Export the theme JSON. Prefer the File System Access API (showSaveFilePicker) +// so re-exporting overwrites the chosen file in place -- a blob download routes +// through the browser's downloads folder, which uniquifies a re-save as +// "name (1).json" rather than replacing it. Fall back to the blob download where +// the API is absent (mirrors importTheme's showOpenFilePicker/fileinput fallback). +async function exportTheme(){ + const data=JSON.stringify(exportObj(),null,1); + if(!window.showSaveFilePicker){const blob=new Blob([data],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=fileSlug()+'.json';a.click();return;} + try{const h=await window.showSaveFilePicker({suggestedName:fileSlug()+'.json',types:[{description:'theme JSON',accept:{'application/json':['.json']}}]}); + const w=await h.createWritable();await w.write(data);await w.close(); + notify('saved "'+fileSlug()+'.json"',false); + }catch(e){if(e&&e.name!=='AbortError')notify('export failed: '+e.message,true);}} function applyImported(text){const d=JSON.parse(text);lastGone={};if(d.name)document.getElementById('themename').value=d.name;if(d.palette)PALETTE=d.palette.map(normalizePaletteEntry); if(!d.syntax)throw new Error('theme JSON is missing syntax; convert older files first'); - SYNTAX={};CATS.forEach(c=>{const k=c[0];SYNTAX[k]=Object.assign(syntaxBlank(k),d.syntax[k]||{});});syncAllSyntaxCache(); + SYNTAX={};CATS.forEach(c=>{const k=c[0];SYNTAX[k]=Object.assign(syntaxBlank(k),migrateLegacyFace(d.syntax[k]||{}));});syncAllSyntaxCache(); LOCKED=new Set(d.locks||[]); - if(d.ui)Object.assign(UIMAP,d.ui); + if(d.ui)for(const k in d.ui)UIMAP[k]=Object.assign(uiFaceBlank(),migrateLegacyFace(d.ui[k])); PKGMAP=seedPkgmap();if(d.packages)mergePackagesInto(PKGMAP,d.packages); refreshPaletteState({pkgPreview:true});updateTitle();} function importFile(ev){const f=ev.target.files[0];if(!f)return;const r=new FileReader(); @@ -381,46 +340,43 @@ async function importTheme(){ // against the new ground for faces without their own bg). function applyGround(){document.querySelectorAll('pre').forEach(p=>p.style.background=MAP['bg']);UI_FACES.forEach(([f])=>{if(document.getElementById('uiprev-'+f))paintUI(f);});} function uf(f){return UIMAP[f]||{};} -function udeco(o){return `font-weight:${o.bold?'bold':'normal'};font-style:${o.italic?'italic':'normal'};text-decoration:${(o.underline?'underline ':'')+(o.strike?'line-through':'')||'none'}`;} -// A face's :box, rendered as an inset box-shadow (no layout shift). Returns the -// box-shadow VALUE (or '' for no box). 'line' is a flat border in the box color -// (or the face's own color when unset); 'released'/'pressed' are the 3D button -// styles Emacs draws, derived from explicit box color when set, otherwise the -// background so they read on any color. -function boxCss(b,bg){if(!b||!b.style)return '';const w=b.width||1; - if(b.style==='released'||b.style==='pressed'){ - // Emacs derives the 3D edges from a base color (reliefColors, ported from - // xterm.c); the translucent pair is only the no-color fallback. - const r=(b.color||bg)?reliefColors(b.color||bg):{hl:null,sh:null}; - const hl=r.hl||'#ffffff33',sh=r.sh||'#00000066'; - const [a,z]=b.style==='released'?[hl,sh]:[sh,hl]; - return `inset ${w}px ${w}px 0 ${a},inset -${w}px -${w}px 0 ${z}`;} - return `inset 0 0 0 ${w}px ${b.color||'currentColor'}`;} -function syntaxStyle(k){const s=syntaxFace(k),fg=(k==='bg'?MAP['p']:resolveSyntaxFg(k,SYNTAX,MAP['p'])),bg=s.bg||null,dec=(s.underline?'underline ':'')+(s.strike?'line-through':''), - bx=boxCss(s.box,bg||MAP['bg']); - return `color:${fg};${bg?'background:'+bg+';':''}font-weight:${s.bold?'bold':'normal'};font-style:${s.italic?'italic':'normal'};text-decoration:${dec.trim()||'none'}${bx?';box-shadow:'+bx:''}`;} +// Map a weight name to a CSS font-weight for the live previews. The named +// weights light/medium/semibold/heavy aren't CSS keywords, so resolve to the +// numeric scale; an unset weight renders normal. +// cssWeight, boxCss, faceDecoration, and faceCss live in app-core.js now. +// udeco keeps its own (untrimmed) decoration form, so it stays here. +function udeco(o){return 'font-weight:'+cssWeight(o.weight)+';font-style:'+(o.slant||'normal')+';text-decoration:'+((o.underline?'underline ':'')+(o.strike?'line-through':'')||'none');} +function syntaxStyle(k){const s=syntaxFace(k),fg=(k==='bg'?MAP['p']:resolveSyntaxFg(k,SYNTAX,MAP['p'])),bg=s.bg||null;return faceCss(s,fg,bg,{boxBg:bg||MAP['bg']});} // The per-row box control: none / line / raised / pressed plus optional line // color. get()/set() read and write the face's box object (null = no box). // Box control: a 2x2 cluster of radio buttons for the four box styles (no box / // line / pressed / raised), plus a compact color swatch shown only while a box // style is active. Replaces the old wide select+swatch to reclaim column width. -function mkBoxControl(get,set,opts={}){const wrap=document.createElement('div');wrap.className='boxctl'; - const cluster=document.createElement('div');cluster.className='boxcluster'; - const states=[['','no box',''],['line','line box','□'],['pressed','pressed','▼'],['released','raised','▲']]; - const btns={}; - states.forEach(([v,title,glyph])=>{const b=document.createElement('button');b.className='boxbtn';b.dataset.style=v;b.textContent=glyph;b.title=title; - b.onclick=()=>{const cur=get();set(v?{style:v,width:(cur&&cur.width)||1,color:(cur&&cur.color)||null}:null);paint();}; - cluster.appendChild(b);btns[v]=b;}); - const dd=mkColorDropdown(ddList((get()&&get().color)||''),(get()&&get().color)||'',h=>{const cur=get();if(!cur)return;set(Object.assign({},cur,{color:h||null}));paint();},{compact:true,defaultHex:opts.defaultHex}); - function paint(){const cur=get(),style=cur&&cur.style?cur.style:''; - for(const v in btns)btns[v].classList.toggle('on',v===style); - dd.style.display=style?'':'none';dd.setValue(cur&&cur.color?cur.color:''); - const locked=wrap.dataset.locked==='1'; - for(const v in btns)btns[v].disabled=locked; - const ddoff=locked||!style;dd.dataset.locked=ddoff?'1':'';dd.classList.toggle('locked',ddoff);if(dd.syncLocked)dd.syncLocked();} - wrap.syncLocked=()=>paint(); - wrap.append(cluster,dd);paint();return wrap;} +// Box control: a 2x2 cluster of the four box styles (no box / line / pressed / +// raised) plus a compact color swatch shown while a style is active. Shares the +// cluster/dropdown/paint machinery with mkLineStyleControl; it differs only in +// that its state object carries `width`, so it passes a toState builder. +function mkBoxControl(get,set,opts={}){ + return mkLineStyleControl( + [['','no box',''],['line','line box','□'],['pressed','pressed','▼'],['released','raised','▲']], + get,set, + Object.assign({styled:true,toState:(v,cur)=>({style:v,width:(cur&&cur.width)||1,color:(cur&&cur.color)||null})},opts));} function flashRow(tr){if(!tr)return;tr.scrollIntoView({block:'center',behavior:'smooth'});tr.classList.remove('flash');void tr.offsetWidth;tr.classList.add('flash');} +// Unified preview-locate click dispatch (preview-locate spec, Phases 4-5). One +// handler for every preview surface replaces the per-surface data-face branches: +// find the clicked data-face element, resolve its owner (data-owner-app, or +// DEFAULTOWNER for a bare span emitted by the generic / auto-dim / UI-mock +// renderers that pre-date previewSpan), and flash its assignment row only when it +// is on-pane. An owner-tagged off-pane / unassigned element is inert; a bare span +// is a current-pane element by construction, so it stays clickable. No persistent +// selection — flashRow is scroll + flash only. The data-k syntax-click path stays +// separate (handled by each caller before delegating here). +function locateClick(e,defaultOwner){ + const u=e.target.closest('[data-face]');if(!u)return; + if(u.dataset.ownerApp&&!u.classList.contains('locate-onpane'))return; + const owner=u.dataset.ownerApp||defaultOwner; + if(owner==='@ui')flashUi(u.dataset.face);else flashPkg(u.dataset.face); +} function flashEl(el){if(!el)return;el.scrollIntoView({block:'nearest',inline:'nearest',behavior:'smooth'});el.classList.remove('flashtok');void el.offsetWidth;el.classList.add('flashtok');} // Flash every matching element but scroll only the first into view, so a face // that maps to several preview spans still lands the viewport on the first. @@ -432,14 +388,15 @@ function flashUiPreview(f){const sp=document.querySelectorAll(`#mockframe [data- function flashPkg(f){flashRow(document.querySelector(`#pkgbody tr[data-face="${f}"]`));} function flashPkgPreview(f){const sp=document.querySelectorAll(`#pkgpreview [data-face="${f}"]`);if(sp.length){flashEls(sp);return;}const row=document.querySelector(`#pkgbody tr[data-face="${f}"]`);if(row)flashEl(row.querySelector('.cat'));} function mockSpan(k,t){return `<span data-k="${k}" style="${syntaxStyle(k)}">${esc(t)}</span>`;} -function uiCss(o,fgv,bgv,opts={}){const fg=fgv===undefined?effFg(o.fg):fgv,bg=bgv===undefined?o.bg:bgv,dec=(o.underline?'underline ':'')+(o.strike?'line-through':''), - bx=boxCss(o.box,bg||MAP['bg']); - return `color:${fg};${bg&&!opts.noBg?'background:'+bg+';':''}font-weight:${o.bold?'bold':'normal'};font-style:${o.italic?'italic':'normal'};text-decoration:${dec.trim()||'none'}${bx?';box-shadow:'+bx:''}`;} -function syncMockHeight(){const t=document.getElementById('uitable'),m=document.getElementById('mockframe');if(!t||!m)return;const lb=m.previousElementSibling,lbh=lb?lb.getBoundingClientRect().height+10:30;m.style.height=Math.max(t.getBoundingClientRect().height-lbh,220)+'px';} +function uiCss(o,fgv,bgv,opts={}){const fg=fgv===undefined?effFg(o.fg):fgv,bg=bgv===undefined?o.bg:bgv;return faceCss(o,fg,bg,{noBg:opts.noBg,boxBg:bg||MAP['bg']});} +// Size a preview pane to its faces table, minus the label bar above it. Shared by +// the UI mock and the package preview, which differ only in their element IDs. +function syncPaneHeight(tableId,paneId){const t=document.getElementById(tableId),m=document.getElementById(paneId);if(!t||!m)return;const lb=m.previousElementSibling,lbh=lb?lb.getBoundingClientRect().height+10:30;m.style.height=Math.max(t.getBoundingClientRect().height-lbh,220)+'px';} function buildMockFrame(){ const fr=document.getElementById('mockframe');if(!fr)return; + rebuildLocateRegistry(); const bg=MAP['bg'],fg=MAP['p']; - const ln=uf('line-number'),lnc=uf('line-number-current-line'),hl=uf('hl-line'),hil=uf('highlight'),reg=uf('region'),isr=uf('isearch'),isf=uf('isearch-fail'),laz=uf('lazy-highlight'),par=uf('show-paren-match'),parx=uf('show-paren-mismatch'),cur=uf('cursor'),ml=uf('mode-line'),mli=uf('mode-line-inactive'),mb=uf('minibuffer-prompt'),frng=uf('fringe'),vb=uf('vertical-border'),lnk=uf('link'),err=uf('error'),wrn=uf('warning'),suc=uf('success'); + const ln=uf('line-number'),lnc=uf('line-number-current-line'),hl=uf('hl-line'),hil=uf('highlight'),reg=uf('region'),isr=uf('isearch'),isf=uf('isearch-fail'),laz=uf('lazy-highlight'),par=uf('show-paren-match'),parx=uf('show-paren-mismatch'),cur=uf('cursor'),ml=uf('mode-line'),mli=uf('mode-line-inactive'),mlh=uf('mode-line-highlight'),mb=uf('minibuffer-prompt'),frng=uf('fringe'),vb=uf('vertical-border'),lnk=uf('link'),err=uf('error'),wrn=uf('warning'),suc=uf('success'); const lines=[ {t:[['cmd',';; '],['cm','init.el - your config']]}, {t:[['punc','('],['kw','require'],['p',' '],['con',"'cl-lib"],['punc',')']]}, @@ -500,12 +457,13 @@ function buildMockFrame(){ buf+=`<div class="ln" ${rowFace?'data-face="hl-line" ':''}style="${rowStyle}"><span class="fr" data-face="fringe" style="${uiCss(frng,frng.fg||fg,frng.bg||bg)};text-align:center;font-size:10px;overflow:hidden" title="fringe">${L.cont?'↪':''}</span><span class="num" data-face="${nFace}" style="${uiCss(isc?lnc:ln,nFg,nBg)}">${i+1}</span><span class="cd">${cd||' '}</span></div>`; }); let html=`<div class="mbuf" style="background:${bg}"><div class="mbuftext">${buf}</div><div class="vborder" data-face="vertical-border" title="vertical-border" style="background:${vb.fg||vb.bg||'#2f343a'}"></div></div>`; - html+=`<div class="bar" data-face="mode-line" style="${uiCss(ml,ml.fg||bg,ml.bg||fg)}"> init.el (Emacs Lisp) L5 git:main </div>`; + const mlhStyle=uiCss(mlh,mlh.fg||ml.fg||bg,mlh.bg||ml.bg||fg); + html+=`<div class="bar" data-face="mode-line" style="${uiCss(ml,ml.fg||bg,ml.bg||fg)}"> init.el (Emacs Lisp) L5 <span data-face="mode-line-highlight" title="mode-line-highlight (hover)" style="${mlhStyle}">git:main</span> </div>`; html+=`<div class="bar" data-face="mode-line-inactive" style="${uiCss(mli,resolveUiAttr('mode-line-inactive','fg',UIMAP)||fg,resolveUiAttr('mode-line-inactive','bg',UIMAP)||bg)}"> *Messages* (Fundamental)</div>`; html+=`<div class="echo" style="color:${fg}"><span data-face="minibuffer-prompt" style="${uiCss(mb,mb.fg||fg,mb.bg||null)}">I-search:</span> count <span data-face="isearch-fail" style="${uiCss(isf,isf.fg||fg,isf.bg||'transparent')}">zzz [no match]</span></div>`; html+=`<div class="echo"><span data-face="link" style="${uiCss(lnk,lnk.fg||fg,lnk.bg||null)}">https://gnu.org</span> <span data-face="error" style="${uiCss(err,err.fg||fg,err.bg||null)}">error</span> <span data-face="warning" style="${uiCss(wrn,wrn.fg||fg,wrn.bg||null)}">warning</span> <span data-face="success" style="${uiCss(suc,suc.fg||fg,suc.bg||null)}">ok</span></div>`; fr.innerHTML=html;fr.style.background=bg;fr.style.color=fg; - fr.onclick=(e)=>{const u=e.target.closest('[data-face]');if(u){flashUi(u.dataset.face);return;}const k=e.target.closest('[data-k]');if(k)flashAssign(k.dataset.k);}; + fr.onclick=(e)=>{if(e.target.closest('[data-face]')){locateClick(e,'@ui');return;}const k=e.target.closest('[data-k]');if(k)flashAssign(k.dataset.k);}; } // All three tiers share one dropdown — the swatch div from mkColorDropdown. The // native <select> rendered swatch colors unreliably on Linux Chrome, so it is @@ -513,452 +471,163 @@ function buildMockFrame(){ function uiSelect(face,attr){const cur=UIMAP[face][attr]||''; return mkColorDropdown(ddList(cur),cur,h=>{UIMAP[face][attr]=h||null;paintUI(face);buildMockFrame();},{compact:true,defaultHex:attr==='fg'?effFg(null):effBg(null)});} const BASE_INHERITS=['fixed-pitch','variable-pitch','default','link','bold','italic','shadow']; -function uiFaceBlank(){return {fg:null,bg:null,bold:false,italic:false,underline:false,strike:false};} -function seedFace(d){return normalizePkgFace({fg:pname(d.fg),bg:pname(d.bg),bold:d.bold,italic:d.italic,underline:d.underline,strike:d.strike,inherit:d.inherit,height:d.height,box:d.box},'default');} +function uiFaceBlank(){return {fg:null,bg:null,'distant-fg':null,family:null,weight:null,slant:null,underline:null,strike:null,overline:null,box:null,inverse:false,extend:false,inherit:null,height:null};} +function seedFace(d){return normalizePkgFace({fg:pname(d.fg),bg:pname(d.bg),'distant-fg':pname(d['distant-fg']),family:d.family,weight:d.weight,slant:d.slant,bold:d.bold,italic:d.italic,underline:d.underline,strike:d.strike,overline:d.overline,inherit:d.inherit,height:d.height,box:d.box,inverse:d.inverse,extend:d.extend},'default');} function curApp(){const s=document.getElementById('viewsel');const v=s&&s.value;return (v&&v[0]!=='@')?v:Object.keys(APPS)[0];} 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. +// Lock keys for one view value (@code / @ui / a package app), so the view +// dropdown can flag a view whose every element is locked. +function viewLockKeys(v){ + if(v==='@code')return syntaxLockKeys(); + if(v==='@ui')return uiLockKeys(); + return (APPS[v]?APPS[v].faces:[]).map(f=>'pkg:'+v+':'+f[0]); +} +// Prefix a lock glyph on every view whose elements are all locked; leave the rest +// bare. The base label rides in dataset.label so re-running never stacks glyphs. +function updateViewLockIndicators(){const s=document.getElementById('viewsel');if(!s)return; + for(const o of s.querySelectorAll('option')){const base=o.dataset.label||o.textContent; + o.textContent=(areAllLocked(viewLockKeys(o.value),LOCKED)?'🔒 ':'')+base;}} 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;}; + const mk=(v,t)=>{const o=document.createElement('option');o.value=v;o.dataset.label=t;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)); - s.appendChild(og);} + for(const app of appViewKeysSorted(APPS))og.appendChild(mk(app,APPS[app].label)); + s.appendChild(og);updateViewLockIndicators();} +// The ‹ › buttons flanking the dropdown step the selection by DIR and re-render +// the view (faces table + preview), so you can walk the list without reopening it. +function stepView(dir){ + const s=document.getElementById('viewsel');if(!s)return; + const i=stepViewIndex(s.selectedIndex,s.options.length,dir); + if(i!==s.selectedIndex){s.selectedIndex=i;onViewChange();} +} +// The ‹ › buttons flanking the language dropdown step the selection by DIR and +// re-render the code sample + package preview, mirroring the view-dropdown nav. +function stepLang(dir){ + const s=document.getElementById('langsel');if(!s)return; + const i=stepViewIndex(s.selectedIndex,s.options.length,dir); + if(i!==s.selectedIndex){s.selectedIndex=i;renderCode();buildPkgPreview();} +} 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';}; show('view-code',v==='@code');show('view-ui',v==='@ui');show('view-pkg',v[0]!=='@'); if(v==='@code')renderCode(); - else if(v==='@ui'){buildUITable();buildMockFrame();syncMockHeight();} + else if(v==='@ui'){buildUITable();buildMockFrame();syncPaneHeight('uitable','mockframe');} else pkgChanged();} -function pkgChanged(){buildPkgTable();buildPkgPreview();syncPkgHeight();} +function pkgChanged(){buildPkgTable();buildPkgPreview();syncPaneHeight('pkgtable','pkgpreview');} function buildPkgTable(){ const app=curApp(),tb=document.getElementById('pkgbody');if(!tb)return;tb.innerHTML=''; const flt=(document.getElementById('pkgfilter').value||'').trim().toLowerCase(); const inh=[''].concat(BASE_INHERITS).concat(APPS[app].faces.map(r=>r[0])); - for(const [face,label] of APPS[app].faces){ + for(const row of APPS[app].faces){ + const face=row[0],label=row[1]; if(flt&&!(face.toLowerCase().includes(flt)||label.toLowerCase().includes(flt)))continue; const f=PKGMAP[app][face],tr=document.createElement('tr');tr.dataset.face=face; - const c0=document.createElement('td');c0.className='cat';c0.textContent=label;c0.title=face;c0.style.cursor='pointer';c0.onclick=()=>flashPkgPreview(face); + const def=normalizePkgFace(row[2]||{},'default',PALETTE); + const nd=faceBoxNonDefaults( + {fg:nameToHex(f.fg,PALETTE),bg:nameToHex(f.bg,PALETTE),weight:f.weight,slant:f.slant,underline:f.underline,strike:f.strike,inherit:f.inherit,height:f.height,box:f.box}, + {fg:nameToHex(def.fg,PALETTE),bg:nameToHex(def.bg,PALETTE),weight:def.weight,slant:def.slant,underline:def.underline,strike:def.strike,inherit:def.inherit,height:def.height,box:def.box}); + const exp=mkExpander(f,tableColCount('pkgtable'),()=>{f.source='user';pkgChanged();},{expandKey:face,showInheritHeight:true,inheritOptions:inh,defaultHex:effFg(pkgEffFg(app,face)),ndCheck:()=>overflowNonDefault(f,def,true)}); + exp.detail.dataset.detailFor=face; + const c0=document.createElement('td');c0.className='cat';c0.title=composeHoverTitle(FACE_DOCS[face],face);c0.appendChild(exp.btn); + const c0lbl=document.createElement('span');c0lbl.textContent=' '+label;c0lbl.style.cursor='pointer';c0lbl.onclick=()=>flashPkgPreview(face);c0.appendChild(c0lbl); const fgd=mkColorDropdown(ddList(f.fg||''),f.fg||'',h=>{f.fg=h||null;f.source='user';pkgChanged();},{compact:true,defaultHex:effFg(pkgEffFg(app,face))}), bgd=mkColorDropdown(ddList(f.bg||''),f.bg||'',h=>{f.bg=h||null;f.source='user';pkgChanged();},{compact:true,defaultHex:effBg(pkgEffBg(app,face))}); const cf=document.createElement('td');cf.appendChild(fgd); const cb=document.createElement('td');cb.appendChild(bgd); const cw=document.createElement('td'); - const pkBtns=mkStyleButtons(at=>f[at],at=>{f[at]=!f[at];f.source='user';pkgChanged();}); - const pkCluster=document.createElement('div');pkCluster.className='stylecluster';pkBtns.forEach(b=>pkCluster.appendChild(b));cw.appendChild(pkCluster); - const ci=document.createElement('td');const isel=document.createElement('select');isel.className='chip';isel.style.cssText='width:150px;font:10pt monospace';inh.forEach(o=>{const op=document.createElement('option');op.value=o;op.textContent=o||'— none —';isel.appendChild(op);});isel.value=f.inherit||'';isel.onchange=()=>{f.inherit=isel.value||null;f.source='user';pkgChanged();};ci.appendChild(isel); - const ch=document.createElement('td');const hin=document.createElement('input');hin.type='number';hin.min='0.8';hin.max='2.5';hin.step='0.05';hin.value=f.height||1;hin.className='hstep';hin.onchange=()=>{f.height=parseFloat(hin.value)||1;f.source='user';pkgChanged();};ch.appendChild(hin); + const pkCtls=mkStyleControls(f,()=>{f.source='user';pkgChanged();},{defaultHex:effFg(pkgEffFg(app,face))}); + const pkCluster=document.createElement('div');pkCluster.className='stylecluster';pkCtls.forEach(c=>pkCluster.appendChild(c));cw.appendChild(pkCluster); const cc=document.createElement('td');cc.style.fontSize='10pt';cc.style.whiteSpace='nowrap';const efg=effFg(pkgEffFg(app,face)),ebg=effBg(pkgEffBg(app,face)),r=contrast(efg,ebg);cc.innerHTML=crHtml(r); const cx=document.createElement('td');const boxCtl=mkBoxControl(()=>f.box,b=>{f.box=b;f.source='user';pkgChanged();},{compact:true});cx.appendChild(boxCtl); - const cL=mkLockCell('pkg:'+app+':'+face,[fgd,bgd,...pkBtns,isel,hin,boxCtl]); - tr.append(c0,cL,cf,cb,cw,cc,ci,ch,cx);tb.appendChild(tr); + const cL=mkLockCell('pkg:'+app+':'+face,[fgd,bgd,...pkCtls,boxCtl,...exp.locks]); + if(nd.fg)cf.classList.add('nd');if(nd.bg)cb.classList.add('nd');if(nd.style)cw.classList.add('nd'); + if(nd.box)cx.classList.add('nd'); + tr.append(cL,c0,cf,cb,cw,cx,cc);tb.appendChild(tr);tb.appendChild(exp.detail); } applyTableSort('pkgbody'); - updateLockToggle('pkg'); -} -function ofs(app,face){const f=PKGMAP[app][face]||{},fg=effFg(pkgEffFg(app,face)),bg=pkgEffBg(app,face);const dec=(f.underline?'underline ':'')+(f.strike?'line-through':'');const bx=boxCss(f.box,bg||MAP['bg']);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${bx?';box-shadow:'+bx:''}`;} -function os(app,face,txt){return `<span data-face="${face}" style="${ofs(app,face)}">${txt}</span>`;} -// Shared wrapper for the line-based package previews: a monospace pre block. -// Each renderer builds its own L array of os(...) lines and returns previewLines(L). -function previewLines(L){return `<div style="padding:12px 16px;font:12pt/1.7 monospace;white-space:pre">${L.join('\n')}</div>`;} -function renderOrgPreview(){const a='org-mode',L=[]; - L.push(os(a,'org-document-info-keyword','#+TITLE:')+' '+os(a,'org-document-title','Project Notes')); - L.push(os(a,'org-document-info-keyword','#+AUTHOR:')+' '+os(a,'org-document-info','Craig Jennings')); - L.push(os(a,'org-meta-line','#+STARTUP: overview')); - L.push(''); - L.push(os(a,'org-level-1','* Inbox')+' '+os(a,'org-tag',':work:')+os(a,'org-tag-group',':@office:')); - L.push(os(a,'org-level-2','** ')+os(a,'org-todo','TODO')+os(a,'org-level-2',' Draft the spec')+' '+os(a,'org-priority','[#A]')+' '+os(a,'org-tag',':spec:')); - L.push(' '+os(a,'org-special-keyword','SCHEDULED:')+' '+os(a,'org-date','<2026-06-08 Sun>')+' '+os(a,'org-special-keyword','DEADLINE:')+' '+os(a,'org-date','<2026-06-12 Thu>')); - L.push(' '+os(a,'org-drawer',':PROPERTIES:')); - L.push(' '+os(a,'org-special-keyword',':ID:')+' '+os(a,'org-property-value','abc-123-def')); - L.push(' '+os(a,'org-drawer',':END:')); - L.push(' '+os(a,'org-list-dt','- term ::')+' definition, with a '+os(a,'org-footnote','[fn:1]')+' note.'); - L.push(' '+os(a,'org-checkbox','[X]')+' done item '+os(a,'org-checkbox-statistics-done','[2/2]')); - L.push(' '+os(a,'org-checkbox','[ ]')+' open item '+os(a,'org-checkbox-statistics-todo','[0/3]')+' '+os(a,'org-warning','(!)')); - L.push(os(a,'org-level-2','** ')+os(a,'org-done','DONE')+os(a,'org-headline-done',' Ship the tool')); - L.push(os(a,'org-level-3','*** ')+os(a,'org-todo','TODO')+os(a,'org-headline-todo',' Heading three')); - L.push(os(a,'org-level-4','**** four')+' / '+os(a,'org-level-5','***** five')+' / '+os(a,'org-level-6','****** six')+' / '+os(a,'org-level-7','******* seven')+' / '+os(a,'org-level-8','******** eight')); - L.push(' Inline '+os(a,'org-code','=code=')+', '+os(a,'org-verbatim','~verbatim~')+', '+os(a,'org-inline-src-block','src_py{1+1}')+','); - L.push(' a '+os(a,'org-link','[[https://gnu.org][link]]')+', a '+os(a,'org-target','<<target>>')+', a '+os(a,'org-macro','{{{macro}}}')+','); - L.push(' a '+os(a,'org-cite','[cite:')+os(a,'org-cite-key','@knuth1984')+os(a,'org-cite',']')+', a date '+os(a,'org-sexp-date','<%%(diary-float 6 5 2)>')+'.'); - L.push(' '+os(a,'org-quote','#+begin_quote')+' a '+os(a,'org-verse','verse')+' line, latex '+os(a,'org-latex-and-related','$E = mc^2$')+'.'); - L.push(''); - L.push(' '+os(a,'org-block-begin-line','#+begin_src elisp')); - L.push(' '+os(a,'org-block',' (message "hi")')); - L.push(' '+os(a,'org-block-end-line','#+end_src')); - L.push(''); - L.push(' '+os(a,'org-table-header','| name | hex |')); - L.push(' '+os(a,'org-table','|------+---------|')); - L.push(' '+os(a,'org-table-row','| blue | #67809c |')+' '+os(a,'org-formula',':=vsum(@2)')); - L.push(' '+os(a,'org-column-title','Effort')+' '+os(a,'org-column','| 0:30 |')+' '+os(a,'org-archived','* archived')+os(a,'org-ellipsis',' ...')); - L.push(''); - L.push(os(a,'org-agenda-structure','Week-agenda (W23):')); - L.push(os(a,'org-agenda-date','Monday 8 June 2026')); - L.push(os(a,'org-agenda-date-today','Tuesday 9 June 2026')+' '+os(a,'org-agenda-current-time','10:24')+' '+os(a,'org-time-grid','----------')); - L.push(os(a,'org-agenda-date-weekend','Saturday 13 June')+' / '+os(a,'org-agenda-date-weekend-today','wknd-today')); - L.push(' '+os(a,'org-scheduled-previously','Sched.past:')+' overdue '+os(a,'org-agenda-done','x done item')); - L.push(' '+os(a,'org-scheduled','Scheduled:')+' a task '+os(a,'org-scheduled-today','due today')); - L.push(' '+os(a,'org-imminent-deadline','Deadline!')+' / '+os(a,'org-upcoming-deadline','upcoming')+' / '+os(a,'org-upcoming-distant-deadline','distant')); - L.push(' '+os(a,'org-agenda-dimmed-todo-face','dimmed todo')+' '+os(a,'org-agenda-diary','diary')+' '+os(a,'org-agenda-clocking','clocking')); - L.push(' '+os(a,'org-agenda-calendar-event','cal-event')+' / '+os(a,'org-agenda-calendar-sexp','cal-sexp')+' / '+os(a,'org-agenda-calendar-daterange','range')); - L.push(' '+os(a,'org-agenda-structure-secondary','secondary')+' '+os(a,'org-agenda-structure-filter','filter')+' '+os(a,'org-agenda-restriction-lock','lock')+' '+os(a,'org-agenda-column-dateline','col-date')); - L.push(' Filters: '+os(a,'org-agenda-filter-category','cat')+' '+os(a,'org-agenda-filter-tags','tags')+' '+os(a,'org-agenda-filter-effort','effort')+' '+os(a,'org-agenda-filter-regexp','re')); - L.push(' '+os(a,'org-mode-line-clock','[0:45]')+' / '+os(a,'org-mode-line-clock-overrun','[OVER]')+' '+os(a,'org-dispatcher-highlight','[d]ispatch')); - return previewLines(L); -} -function renderMagitPreview(){const a='magit',L=[]; - L.push(os(a,'magit-header-line',' Magit: dotemacs ')+' '+os(a,'magit-header-line-key','g')+os(a,'magit-header-line-log-select',' refresh')); - L.push(os(a,'magit-head','Head:')+' '+os(a,'magit-branch-current','main')+' '+os(a,'magit-diff-revision-summary','Ship the tool')); - L.push(os(a,'magit-head','Merge:')+' '+os(a,'magit-branch-remote','origin/main')+' '+os(a,'magit-branch-local','main')); - L.push(os(a,'magit-head','Push:')+' '+os(a,'magit-branch-remote-head','origin/main')); - L.push(os(a,'magit-head','Upstream:')+' '+os(a,'magit-branch-upstream','origin/main')+' '+os(a,'magit-branch-warning','(diverged)')); - L.push(''); - L.push(os(a,'magit-section-heading','Untracked files')+' '+os(a,'magit-section-child-count','(2)')); - L.push(' '+os(a,'magit-filename','notes.txt')+' '+os(a,'magit-dimmed','(ignored sibling)')); - L.push(os(a,'magit-section-highlight',' scratch.el (highlighted row)')); - L.push(''); - L.push(os(a,'magit-section-heading','Unstaged changes')+' '+os(a,'magit-section-child-count','(1)')); - L.push(os(a,'magit-diff-file-heading','modified generate.py')); - L.push(os(a,'magit-diff-hunk-heading','@@ -1,4 +1,5 @@ def main')); - L.push(os(a,'magit-diff-context',' unchanged context')); - L.push(os(a,'magit-diff-removed','- old line')+os(a,'magit-diff-whitespace-warning',' ')); - L.push(os(a,'magit-diff-added','+ new line')); - L.push(''); - L.push(os(a,'magit-section-heading','Staged changes')+' '+os(a,'magit-diffstat-added','++++')+os(a,'magit-diffstat-removed','--')); - L.push(os(a,'magit-diff-file-heading-highlight','modified README.md (highlighted heading)')); - L.push(os(a,'magit-diff-hunk-heading-highlight','@@ hunk heading highlight @@')); - L.push(os(a,'magit-diff-added-highlight','+ added highlight')+' '+os(a,'magit-diff-removed-highlight','- removed highlight')); - L.push(os(a,'magit-diff-context-highlight',' context highlight')); - L.push(''); - L.push(os(a,'magit-section-heading','Stashes')); - L.push(' '+os(a,'magit-refname-stash','stash@{0}')+' '+os(a,'magit-refname-wip','wip')+' '+os(a,'magit-refname-pullreq','pr/42')+' '+os(a,'magit-refname','refs/heads/x')); - L.push(''); - L.push(os(a,'magit-section-heading','Recent commits')); - L.push(os(a,'magit-log-graph','* ')+os(a,'magit-hash','b5b1869f')+' '+os(a,'magit-log-date','06-08')+' '+os(a,'magit-log-author','Craig')+' enlarge the picker'); - L.push(os(a,'magit-log-graph','* ')+os(a,'magit-hash','4fa5e995')+' '+os(a,'magit-log-date','06-07')+' '+os(a,'magit-log-author','Craig')+' '+os(a,'magit-keyword','[feat]')+' picker'); - L.push(os(a,'magit-log-graph','* ')+os(a,'magit-hash','de07e01a')+' '+os(a,'magit-log-date','06-05')+' '+os(a,'magit-log-author','Craig')+' '+os(a,'magit-tag','v0.3')+' '+os(a,'magit-keyword-squash','!squash')); - L.push(''); - L.push(os(a,'magit-section-secondary-heading','Merge conflict')+' '+os(a,'magit-diff-lines-heading','lines 10-14')+os(a,'magit-diff-lines-boundary','|')); - L.push(' '+os(a,'magit-diff-conflict-heading','=======')+' '+os(a,'magit-diff-conflict-heading-highlight','(hl)')); - L.push(' '+os(a,'magit-diff-base','base')+'/'+os(a,'magit-diff-base-highlight','base-hl')+' '+os(a,'magit-diff-our','ours')+'/'+os(a,'magit-diff-our-highlight','ours-hl')+' '+os(a,'magit-diff-their','theirs')+'/'+os(a,'magit-diff-their-highlight','theirs-hl')); - L.push(' '+os(a,'magit-diff-hunk-region','hunk-region')+' '+os(a,'magit-diff-file-heading-selection','file-sel')+' '+os(a,'magit-diff-hunk-heading-selection','hunk-sel')+' '+os(a,'magit-section-heading-selection','sec-sel')+' '+os(a,'magit-diff-revision-summary-highlight','rev-sum-hl')); - L.push(''); - L.push(os(a,'magit-section-heading','Reflog')); - L.push(' '+os(a,'magit-reflog-commit','commit')+' '+os(a,'magit-reflog-amend','amend')+' '+os(a,'magit-reflog-merge','merge')+' '+os(a,'magit-reflog-checkout','checkout')+' '+os(a,'magit-reflog-reset','reset')+' '+os(a,'magit-reflog-rebase','rebase')+' '+os(a,'magit-reflog-cherry-pick','cherry-pick')+' '+os(a,'magit-reflog-remote','remote')+' '+os(a,'magit-reflog-other','other')); - L.push(os(a,'magit-section-heading','Rebase sequence')); - L.push(' '+os(a,'magit-sequence-pick','pick')+' '+os(a,'magit-sequence-stop','stop')+' '+os(a,'magit-sequence-part','part')+' '+os(a,'magit-sequence-head','head')+' '+os(a,'magit-sequence-drop','drop')+' '+os(a,'magit-sequence-done','done')+' '+os(a,'magit-sequence-onto','onto')+' '+os(a,'magit-sequence-exec','exec')); - L.push(os(a,'magit-section-heading','Bisect / Cherry / Process')); - L.push(' '+os(a,'magit-bisect-good','good')+' '+os(a,'magit-bisect-bad','bad')+' '+os(a,'magit-bisect-skip','skip')+' '+os(a,'magit-cherry-equivalent','equivalent')+' '+os(a,'magit-cherry-unmatched','unmatched')); - L.push(' '+os(a,'magit-process-ok','OK')+' '+os(a,'magit-process-ng','NG')+' '+os(a,'magit-mode-line-process','[fetch]')+' '+os(a,'magit-mode-line-process-error','[error]')); - L.push(os(a,'magit-section-heading','Blame')); - L.push(os(a,'magit-blame-margin','margin')+os(a,'magit-blame-heading',' b5b1869f ')) - L.push(' '+os(a,'magit-blame-hash','b5b1869f')+' '+os(a,'magit-blame-name','Craig')+' '+os(a,'magit-blame-date','2026-06-08')+' '+os(a,'magit-blame-summary','enlarge picker')+' '+os(a,'magit-blame-highlight','hl')+' '+os(a,'magit-blame-dimmed','dim')); - L.push(os(a,'magit-section-heading','Signatures')+os(a,'magit-left-margin',' ')); - L.push(' '+os(a,'magit-signature-good','good')+' '+os(a,'magit-signature-bad','bad')+' '+os(a,'magit-signature-untrusted','untrusted')+' '+os(a,'magit-signature-expired','expired')+' '+os(a,'magit-signature-expired-key','expired-key')+' '+os(a,'magit-signature-revoked','revoked')+' '+os(a,'magit-signature-error','error')); - return previewLines(L);} -function renderElfeedPreview(){const a='elfeed',L=[]; - L.push(os(a,'elfeed-search-filter-face','@6-months-ago +unread')+' '+os(a,'elfeed-search-unread-count-face','3/120')+' '+os(a,'elfeed-search-last-update-face','updated 02:24')); - L.push(''); - L.push(os(a,'elfeed-search-date-face','2026-06-08')+' '+os(a,'elfeed-search-feed-face','Planet Emacs')+' '+os(a,'elfeed-search-unread-title-face','New release of Magit')+' '+os(a,'elfeed-search-tag-face',':emacs:')); - L.push(os(a,'elfeed-search-date-face','2026-06-07')+' '+os(a,'elfeed-search-feed-face','LWN')+' '+os(a,'elfeed-search-unread-title-face','Kernel 6.18 lands')+' '+os(a,'elfeed-search-tag-face',':linux:')); - L.push(os(a,'elfeed-search-date-face','2026-06-05')+' '+os(a,'elfeed-search-feed-face','Hacker News')+' '+os(a,'elfeed-search-title-face','Show HN: a theme editor')+' '+os(a,'elfeed-search-tag-face',':show:')); - L.push(''); - L.push(os(a,'elfeed-log-date-face','02:24:01')+' '+os(a,'elfeed-log-info-level-face','INFO ')+' updated 12 feeds'); - L.push(os(a,'elfeed-log-date-face','02:24:02')+' '+os(a,'elfeed-log-warn-level-face','WARN ')+' slow feed: example.com'); - L.push(os(a,'elfeed-log-date-face','02:24:03')+' '+os(a,'elfeed-log-error-level-face','ERROR')+' failed: bad.example'); - L.push(os(a,'elfeed-log-date-face','02:24:04')+' '+os(a,'elfeed-log-debug-level-face','DEBUG')+' parsed 340 entries'); - return previewLines(L);} -function renderGhostelPreview(){const a='ghostel',L=[]; - L.push(os(a,'ghostel-default','craig@host')+' '+os(a,'ghostel-color-green','~/code')+' $ ls'+os(a,'ghostel-fake-cursor',' ')+os(a,'ghostel-fake-cursor-box','[ ]')); - L.push(''); - L.push(os(a,'ghostel-default','normal:')+' '+os(a,'ghostel-color-black','black')+' '+os(a,'ghostel-color-red','red')+' '+os(a,'ghostel-color-green','green')+' '+os(a,'ghostel-color-yellow','yellow')+' '+os(a,'ghostel-color-blue','blue')+' '+os(a,'ghostel-color-magenta','magenta')+' '+os(a,'ghostel-color-cyan','cyan')+' '+os(a,'ghostel-color-white','white')); - L.push(os(a,'ghostel-default','bright:')+' '+os(a,'ghostel-color-bright-black','black')+' '+os(a,'ghostel-color-bright-red','red')+' '+os(a,'ghostel-color-bright-green','green')+' '+os(a,'ghostel-color-bright-yellow','yellow')+' '+os(a,'ghostel-color-bright-blue','blue')+' '+os(a,'ghostel-color-bright-magenta','magenta')+' '+os(a,'ghostel-color-bright-cyan','cyan')+' '+os(a,'ghostel-color-bright-white','white')); - L.push(''); - L.push(os(a,'ghostel-default','default terminal output, 256-color text and a blinking ')+os(a,'ghostel-fake-cursor','cursor')+'.'); - return previewLines(L);} -function renderDashboardPreview(){const a='dashboard',L=[]; - L.push(os(a,'dashboard-text-banner',' [ dashboard banner image ]')); - L.push(os(a,'dashboard-banner-logo-title','Emacs: The Editor That Saves Your Soul')); - L.push(''); - L.push(os(a,'dashboard-navigator',' Code Files Terminal Agenda')); - L.push(os(a,'dashboard-navigator',' Feeds Books Flashcards Music')); - L.push(os(a,'dashboard-navigator',' Email IRC Telegram')); - L.push(os(a,'dashboard-navigator',' Slack Linear')); - L.push(''); - L.push(''); - L.push(os(a,'dashboard-heading','Projects:')); - L.push(' ~/'); - L.push(' ~/.emacs.d/'); - L.push(' ~/projects/work/'); - L.push(' ~/org/roam/'); - L.push(' ~/projects/home/'); - L.push(''); - L.push(os(a,'dashboard-heading','Bookmarks')); - L.push(' Cesar Aira, The Little Buddhist Monk & the Proof'); - L.push(' Edward Abbey, The Fool’s Progress: An Honest Novel'); - L.push(' Agatha Christie, The A.B.C. Murders'); - L.push(''); - L.push(os(a,'dashboard-heading','Recent Files:')); - L.push(' theme-theme.el'); - L.push(' todo.org'); - L.push(' theme-studio-palette-generator-spec.org'); - return previewLines(L);} -function renderMu4ePreview(){const a='mu4e',L=[]; - L.push(os(a,'mu4e-title-face','mu4e')+' '+os(a,'mu4e-context-face','[Personal]')+' '+os(a,'mu4e-ok-face','online')+' '+os(a,'mu4e-warning-face','2 retry')+' '+os(a,'mu4e-modeline-face','12/340')); - L.push(''); - L.push(os(a,'mu4e-header-title-face','Date Flags From Subject')); - L.push(os(a,'mu4e-header-value-face','2026-06-08')+' '+os(a,'mu4e-header-marks-face','N')+' '+os(a,'mu4e-unread-face','Christine')+' '+os(a,'mu4e-unread-face','Unread message')); - L.push(os(a,'mu4e-header-value-face','2026-06-07')+' '+os(a,'mu4e-header-marks-face','R')+' '+os(a,'mu4e-header-face','Bob')+' '+os(a,'mu4e-replied-face','Replied thread')); - L.push(os(a,'mu4e-header-value-face','2026-06-06')+' '+os(a,'mu4e-header-marks-face','F')+' '+os(a,'mu4e-header-face','Carol')+' '+os(a,'mu4e-forwarded-face','Forwarded note')); - L.push(os(a,'mu4e-header-value-face','2026-06-05')+' '+os(a,'mu4e-header-marks-face','D')+' '+os(a,'mu4e-draft-face','(draft)')+' '+os(a,'mu4e-draft-face','Draft in progress')); - L.push(os(a,'mu4e-header-value-face','2026-06-04')+' '+os(a,'mu4e-header-marks-face','T')+' '+os(a,'mu4e-trashed-face','Dan')+' '+os(a,'mu4e-moved-face','Trashed and moved')); - L.push(os(a,'mu4e-header-highlight-face','2026-06-03 ! Evan Flagged ')+os(a,'mu4e-flagged-face','important')+os(a,'mu4e-related-face',' (related)')); - L.push(''); - L.push(os(a,'mu4e-header-key-face','From:')+' '+os(a,'mu4e-contact-face','Christine <christine@example.com>')); - L.push(os(a,'mu4e-header-key-face','To:')+' '+os(a,'mu4e-special-header-value-face','craig, list@gnu.org')); - L.push(os(a,'mu4e-header-key-face','Attach:')+' '+os(a,'mu4e-attach-number-face','[1]')+' report.pdf link '+os(a,'mu4e-url-number-face','[2]')+' '+os(a,'mu4e-link-face','https://gnu.org')); - L.push(''); - L.push(' body with a '+os(a,'mu4e-highlight-face','search hit')+' and '+os(a,'mu4e-region-code','code region')+'.'); - L.push(' '+os(a,'mu4e-cited-1-face','> level 1')+' '+os(a,'mu4e-cited-2-face','>> 2')+' '+os(a,'mu4e-cited-3-face','>>> 3')+' '+os(a,'mu4e-cited-4-face','> 4')+' '+os(a,'mu4e-cited-5-face','> 5')+' '+os(a,'mu4e-cited-6-face','> 6')+' '+os(a,'mu4e-cited-7-face','> 7')); - L.push(' '+os(a,'mu4e-system-face','*** system message ***')+' '+os(a,'mu4e-footer-face','-- sent with mu4e')); - L.push(''); - L.push(os(a,'mu4e-compose-header-face','Subject:')+' new mail'); - L.push(os(a,'mu4e-compose-separator-face','--text follows this line--')); - return previewLines(L);} -function renderOrgFacesPreview(){const a='org-faces',L=[]; - L.push('Agenda header row -- one face per keyword and priority (this config, not built-in org):'); - L.push(''); - L.push(os(a,'org-faces-todo','TODO')+' Draft the spec '+os(a,'org-faces-priority-a','[#A]')); - L.push(os(a,'org-faces-project','PROJECT')+' Theme studio overhaul '+os(a,'org-faces-priority-b','[#B]')); - L.push(os(a,'org-faces-doing','DOING')+' Wire the faces '+os(a,'org-faces-priority-c','[#C]')); - L.push(os(a,'org-faces-waiting','WAITING')+' On review '+os(a,'org-faces-priority-d','[#D]')); - L.push(os(a,'org-faces-verify','VERIFY')+' Confirm the round-trip'); - L.push(os(a,'org-faces-stalled','STALLED')+' Blocked on upstream'); - L.push(os(a,'org-faces-delegated','DELEGATED')+' Handed to Kostya'); - L.push(os(a,'org-faces-failed','FAILED')+' Could not reproduce'); - L.push(os(a,'org-faces-done','DONE')+' Shipped the module'); - L.push(os(a,'org-faces-cancelled','CANCELLED')+' Dropped the approach'); - L.push(''); - L.push('Unfocused (auto-dim) -- the -dim variants auto-dim remaps onto in non-selected windows:'); - L.push(''); - L.push(os(a,'org-faces-todo-dim','TODO')+' Draft the spec '+os(a,'org-faces-priority-a-dim','[#A]')); - L.push(os(a,'org-faces-project-dim','PROJECT')+' Theme studio overhaul '+os(a,'org-faces-priority-b-dim','[#B]')); - L.push(os(a,'org-faces-doing-dim','DOING')+' Wire the faces '+os(a,'org-faces-priority-c-dim','[#C]')); - L.push(os(a,'org-faces-waiting-dim','WAITING')+' On review '+os(a,'org-faces-priority-d-dim','[#D]')); - L.push(os(a,'org-faces-verify-dim','VERIFY')+' Confirm the round-trip'); - L.push(os(a,'org-faces-stalled-dim','STALLED')+' Blocked on upstream'); - L.push(os(a,'org-faces-delegated-dim','DELEGATED')+' Handed to Kostya'); - L.push(os(a,'org-faces-failed-dim','FAILED')+' Could not reproduce'); - L.push(os(a,'org-faces-done-dim','DONE')+' Shipped the module'); - L.push(os(a,'org-faces-cancelled-dim','CANCELLED')+' Dropped the approach'); - return previewLines(L);} -function renderLspPreview(){const a='lsp-mode',L=[]; - L.push(os(a,'lsp-signature-face','process(')+os(a,'lsp-signature-highlight-function-argument','items: list')+os(a,'lsp-signature-face',') -> None')); - L.push(os(a,'lsp-signature-posframe',' docs: iterate over items and process each one ')); - L.push(''); - L.push('def process(items):'); - L.push(' n = len(items)'+os(a,'lsp-inlay-hint-type-face',': int')); - L.push(' handle('+os(a,'lsp-inlay-hint-parameter-face','arg:')+'n)'+os(a,'lsp-inlay-hint-face',' # hint')); - L.push(' '+os(a,'lsp-face-highlight-read','value')+' = '+os(a,'lsp-face-highlight-write','value')+' + '+os(a,'lsp-face-highlight-textual','value')); - L.push(' rename '+os(a,'lsp-face-rename','oldName')+' to '+os(a,'lsp-rename-placeholder-face','newName')); - L.push(' getName() '+os(a,'lsp-details-face','str the cached getter')); - L.push(''); - L.push(os(a,'lsp-installation-buffer-face','Installing pyright...')+' '+os(a,'lsp-installation-finished-buffer-face','done.')); - return previewLines(L);} -function renderGitGutterPreview(){const a='git-gutter',L=[]; - L.push(os(a,'git-gutter:added','+')+os(a,'git-gutter:separator','|')+' added line of code'); - L.push(os(a,'git-gutter:modified','~')+os(a,'git-gutter:separator','|')+' modified line of code'); - L.push(os(a,'git-gutter:deleted','_')+os(a,'git-gutter:separator','|')+' (deleted lines marker)'); - L.push(os(a,'git-gutter:unchanged',' ')+os(a,'git-gutter:separator','|')+' '+os(a,'git-gutter:unchanged','unchanged line of code')); - return previewLines(L);} -function renderFlycheckPreview(){const a='flycheck',L=[]; - L.push(os(a,'flycheck-fringe-error','E')+os(a,'flycheck-fringe-warning','W')+os(a,'flycheck-fringe-info','I')+' x = '+os(a,'flycheck-error','undefined_name')+'('+os(a,'flycheck-warning','unused_arg')+') '+os(a,'flycheck-info','# note')); - L.push(' '+os(a,'flycheck-error-delimiter','[')+os(a,'flycheck-delimited-error','err')+os(a,'flycheck-error-delimiter',']')); - L.push(''); - L.push(os(a,'flycheck-error-list-checker-name','pyright')+' '+os(a,'flycheck-verify-select-checker','(selected checker)')); - L.push(os(a,'flycheck-error-list-filename','main.py')+':'+os(a,'flycheck-error-list-line-number','12')+':'+os(a,'flycheck-error-list-column-number','4')+' '+os(a,'flycheck-error-list-error','error')+' '+os(a,'flycheck-error-list-error-message','undefined name x')+' '+os(a,'flycheck-error-list-id','[E0602]')); - L.push(os(a,'flycheck-error-list-filename','main.py')+':'+os(a,'flycheck-error-list-line-number','18')+':'+os(a,'flycheck-error-list-column-number','1')+' '+os(a,'flycheck-error-list-warning','warning')+' '+os(a,'flycheck-error-list-error-message','unused import')+' '+os(a,'flycheck-error-list-id-with-explainer','[W0611?]')); - L.push(os(a,'flycheck-error-list-highlight','main.py:20 '+os(a,'flycheck-error-list-info','info')+' highlighted row')); - return previewLines(L);} -function renderDiredPreview(){const a='dired',L=[]; - L.push(os(a,'dired-header','/home/craig/code:')); - L.push(' '+os(a,'dired-perm-write','drwxr-xr-x')+' craig 4096 '+os(a,'dired-directory','src/')); - L.push(' -rw-r--r-- craig 120 notes.org'); - L.push(' '+os(a,'dired-perm-write','lrwxrwxrwx')+' craig 18 '+os(a,'dired-symlink','latest -> v2.1')); - L.push(' lrwxrwxrwx craig -- '+os(a,'dired-broken-symlink','dead -> gone')); - L.push(os(a,'dired-flagged','D')+' -rw-r--r-- craig 40 deleteme.tmp'); - L.push(os(a,'dired-mark','*')+' '+os(a,'dired-marked','-rw-r--r-- craig 210 marked.txt')); - L.push(' -rw-r--r-- craig 0 '+os(a,'dired-ignored','.gitignore')); - L.push(' '+os(a,'dired-set-id','-rwsr-xr-x')+' root 900 setuid.bin'); - L.push(' '+os(a,'dired-special','prw-r--r--')+' craig 0 fifo.pipe'); - L.push(os(a,'dired-warning','! disk space low on /home')); - return previewLines(L);} -function renderDirvishPreview(){const a='dirvish',L=[]; - L.push(os(a,'dirvish-inactive','~/code')+' '+os(a,'dirvish-free-space','[free 24G]')); - L.push(os(a,'dirvish-hl-line',' '+os(a,'dirvish-file-modes','-rw-r--r--')+' '+os(a,'dirvish-file-link-number','1')+' '+os(a,'dirvish-file-user-id','craig')+' '+os(a,'dirvish-file-group-id','staff')+' '+os(a,'dirvish-file-size','4.0K')+' '+os(a,'dirvish-file-time','Jun 8 02:24')+' init.el ')); - L.push(' '+os(a,'dirvish-file-modes','drwxr-xr-x')+' '+os(a,'dirvish-file-link-number','5')+' '+os(a,'dirvish-file-user-id','craig')+' '+os(a,'dirvish-file-group-id','staff')+' '+os(a,'dirvish-file-size',' - ')+' '+os(a,'dirvish-file-time','Jun 7 18:00')+' '+os(a,'dirvish-collapse-dir-face','src')+os(a,'dirvish-subtree-state','+')+os(a,'dirvish-subtree-guide',' |')); - L.push(os(a,'dirvish-hl-line-inactive',' inactive-window current line ')); - L.push(' inode '+os(a,'dirvish-file-inode-number','1048576')+' dev '+os(a,'dirvish-file-device-number','8,1')+' '+os(a,'dirvish-collapse-empty-dir-face','empty/')+' '+os(a,'dirvish-collapse-file-face','file.txt')); - L.push(' VC '+os(a,'dirvish-vc-added-state','A')+os(a,'dirvish-vc-edited-state','M')+os(a,'dirvish-vc-removed-state','D')+os(a,'dirvish-vc-conflict-state','C')+os(a,'dirvish-vc-locked-state','L')+os(a,'dirvish-vc-missing-state','!')+os(a,'dirvish-vc-needs-merge-face','m')+os(a,'dirvish-vc-needs-update-state','u')+os(a,'dirvish-vc-unregistered-face','?')); - L.push(' git '+os(a,'dirvish-git-commit-message-face','feat: enlarge the picker')); - L.push(' '+os(a,'dirvish-media-info-heading','Media')+' '+os(a,'dirvish-media-info-property-key','Dimensions:')+' 1920x1080'); - L.push(' proc '+os(a,'dirvish-proc-running','running')+' / '+os(a,'dirvish-proc-finished','finished')+' / '+os(a,'dirvish-proc-failed','failed')); - L.push(' narrow '+os(a,'dirvish-narrow-match-face-0','m0')+' '+os(a,'dirvish-narrow-match-face-1','m1')+' '+os(a,'dirvish-narrow-match-face-2','m2')+' '+os(a,'dirvish-narrow-match-face-3','m3')+os(a,'dirvish-narrow-split',' | ')+os(a,'dirvish-emerge-group-title','Group: images')); - return previewLines(L);} -function renderCalibredbPreview(){const a='calibredb',L=[]; - L.push(os(a,'calibredb-search-header-library-name-face','Calibre')+' '+os(a,'calibredb-search-header-library-path-face','~/books')+' '+os(a,'calibredb-search-header-total-face','412 books')+' '+os(a,'calibredb-search-header-filter-face','tag:scifi')+' '+os(a,'calibredb-search-header-sort-face','sort:date')+' '+os(a,'calibredb-search-header-highlight-face','[*]')); - L.push(''); - L.push(os(a,'calibredb-id-face','1')+' '+os(a,'calibredb-title-face','Dune')+' '+os(a,'calibredb-author-face','Herbert')+' '+os(a,'calibredb-format-face','EPUB')+' '+os(a,'calibredb-size-face','2.1M')+' '+os(a,'calibredb-tag-face',':scifi:')+' '+os(a,'calibredb-date-face','2026-06-08')); - L.push(os(a,'calibredb-mark-face','*')+os(a,'calibredb-id-face','2')+' '+os(a,'calibredb-title-face','Foundation')+' '+os(a,'calibredb-author-face','Asimov')+' '+os(a,'calibredb-series-face','[Foundation #1]')+' '+os(a,'calibredb-publisher-face','Bantam')+' '+os(a,'calibredb-pubdate-face','1951')); - L.push(''); - L.push(os(a,'calibredb-title-detailed-view-face','Foundation (detailed)')+' '+os(a,'calibredb-language-face','eng')+' '+os(a,'calibredb-favorite-face','* fav')+' '+os(a,'calibredb-archive-face','archived')); - L.push(os(a,'calibredb-ids-face','isbn:0553293354')+' '+os(a,'calibredb-file-face','foundation.epub')+' '+os(a,'calibredb-comment-face','A classic of the genre.')); - L.push(os(a,'calibredb-edit-annotation-header-title-face','Annotations')+' '+os(a,'calibredb-highlight-face','highlighted passage')+' '+os(a,'calibredb-current-page-button-face','[page 42]')+' '+os(a,'calibredb-mouse-face','hover row')); - return previewLines(L);} -function renderErcPreview(){const a='erc',L=[]; - L.push(os(a,'erc-header-line',' #emacs on Libera.Chat 18 users ')); - L.push(os(a,'erc-timestamp-face','[10:24]')+' '+os(a,'erc-notice-face','*** alice has joined #emacs')); - L.push(os(a,'erc-timestamp-face','[10:25]')+' <'+os(a,'erc-my-nick-prefix-face','@')+os(a,'erc-my-nick-face','craig')+'> '+os(a,'erc-input-face','hello everyone')); - L.push(os(a,'erc-timestamp-face','[10:25]')+' <'+os(a,'erc-nick-prefix-face','+')+os(a,'erc-nick-default-face','bob')+'> '+os(a,'erc-default-face','hi craig, see ')+os(a,'erc-button','this link')+os(a,'erc-default-face',' cc ')+os(a,'erc-button-nick-default-face','@alice')); - L.push(os(a,'erc-timestamp-face','[10:26]')+' '+os(a,'erc-action-face','* craig waves')+' '+os(a,'erc-keyword-face','emacs')+' '+os(a,'erc-pal-face','<friend>')+' '+os(a,'erc-fool-face','<troll>')+' '+os(a,'erc-dangerous-host-face','<bad@host>')); - L.push(os(a,'erc-timestamp-face','[10:27]')+' '+os(a,'erc-direct-msg-face','(DM)')+' <'+os(a,'erc-nick-msg-face','bob')+'> psst '+os(a,'erc-current-nick-face','craig')+' '+os(a,'erc-information','-info-')); - L.push(os(a,'erc-error-face','*** ERROR: connection reset')); - L.push(os(a,'erc-command-indicator-face','/help')+' '+os(a,'erc-bold-face','bold')+' '+os(a,'erc-italic-face','italic')+' '+os(a,'erc-underline-face','underline')+' '+os(a,'erc-inverse-face','inverse')+' '+os(a,'erc-spoiler-face','spoiler')); - L.push(os(a,'erc-keep-place-indicator-arrow','>')+os(a,'erc-keep-place-indicator-line',' ---- last read ---- ')+os(a,'erc-fill-wrap-merge-indicator-face','+')); - L.push(os(a,'erc-prompt-face','craig>')+' '+os(a,'erc-input-face','type a message...')); - return previewLines(L);} -function renderOrgdrillPreview(){const a='org-drill',L=[]; - L.push('Q: The capital of France is '+os(a,'org-drill-hidden-cloze-face','[...]')+'.'); - L.push('A: The capital of France is '+os(a,'org-drill-visible-cloze-face','Paris')+'.'); - L.push(' '+os(a,'org-drill-visible-cloze-hint-face','hint: P____')); - return previewLines(L);} -function renderOrgnoterPreview(){const a='org-noter',L=[]; - L.push('org-noter paper.pdf'); - L.push(' page 1 '+os(a,'org-noter-notes-exist-face','[notes]')); - L.push(' page 2 '+os(a,'org-noter-no-notes-exist-face','[no notes]')); - return previewLines(L);} -function renderSignelPreview(){const a='signel',L=[]; - L.push(os(a,'signel-timestamp-face','[10:24]')+' '+os(a,'signel-my-msg-face','Me: hey, are we still on for tonight?')); - L.push(os(a,'signel-timestamp-face','[10:25]')+' '+os(a,'signel-other-msg-face','Christine: yes! see you at 7')); - L.push(os(a,'signel-error-face','(failed to send -- retrying)')); - return previewLines(L);} -function renderPearlPreview(){const a='pearl',L=[]; - L.push(os(a,'pearl-preamble-summary','PEARL-42 Fix the broken picker')); - L.push('State: '+os(a,'pearl-modified-local','In Progress')+' Priority: '+os(a,'pearl-modified-highlight','High')+' Estimate: '+os(a,'pearl-modified-unknown','?')); - L.push(' '+os(a,'pearl-editable-comment','> add a comment (editable)')); - L.push(' '+os(a,'pearl-readonly-comment','> created by automation (read-only)')); - return previewLines(L);} -function renderShrPreview(){const a='shr',L=[]; - L.push(os(a,'shr-text','shr renders nov (EPUB), eww (web), elfeed, and HTML mail.')); - L.push(''); - L.push(os(a,'shr-h1','Chapter One: The Beginning')); - L.push(os(a,'shr-h2','A Section Heading')); - L.push(os(a,'shr-h3','A subsection')+' '+os(a,'shr-h4','h4')+' / '+os(a,'shr-h5','h5')+' / '+os(a,'shr-h6','h6')); - L.push(os(a,'shr-text','Body text flows in shr-text, with a ')+os(a,'shr-link','hyperlink')+os(a,'shr-text',' and a ')+os(a,'shr-selected-link','focused link')+os(a,'shr-text',',')); - L.push(os(a,'shr-text','some ')+os(a,'shr-code','inline_code()')+os(a,'shr-text',', a ')+os(a,'shr-mark','highlighted mark')+os(a,'shr-text',', ')+os(a,'shr-strike-through','struck out')+os(a,'shr-text',', a footnote')+os(a,'shr-sup','[1]')+os(a,'shr-text',',')); - L.push(os(a,'shr-text','an ')+os(a,'shr-abbreviation','HTML')+os(a,'shr-text',' abbreviation, and an ')+os(a,'shr-sliced-image','[image]')+os(a,'shr-text',' slice.')); - return previewLines(L);} -function renderSlackPreview(){const a='slack',L=[]; - L.push(os(a,'slack-room-info-title-room-name-face','#general')+' '+os(a,'slack-room-info-title-face','Acme Workspace')); - L.push(os(a,'slack-room-info-section-title-face','Topic')+' '+os(a,'slack-room-info-section-label-face','daily standup')+' '+os(a,'slack-room-unread-face','3 unread')); - L.push(os(a,'slack-new-message-marker-face','---------------- new messages ----------------')); - L.push(os(a,'slack-message-output-header','craig 10:24')); - L.push(' '+os(a,'slack-message-output-text','hey ')+os(a,'slack-message-mention-me-face','@craig')+os(a,'slack-message-output-text',', see ')+os(a,'slack-message-mention-face','@alice')+os(a,'slack-message-output-text',' in ')+os(a,'slack-channel-button-face','#general')+' '+os(a,'slack-message-mention-keyword-face','urgent')); - L.push(' '+os(a,'slack-mrkdwn-bold-face','*bold*')+' '+os(a,'slack-mrkdwn-italic-face','_italic_')+' '+os(a,'slack-mrkdwn-code-face','`code`')+' '+os(a,'slack-mrkdwn-strike-face','~strike~')); - L.push(' '+os(a,'slack-mrkdwn-blockquote-face','> quoted')+' '+os(a,'slack-mrkdwn-list-face','- item')); - L.push(' '+os(a,'slack-mrkdwn-code-block-face','``` code block ```')); - L.push(' '+os(a,'slack-message-output-reaction',':thumbsup: 3')+' '+os(a,'slack-message-output-reaction-pressed',':heart: 1')+' '+os(a,'slack-message-deleted-face','(message deleted)')); - L.push(' '+os(a,'slack-all-thread-buffer-thread-header-face','Thread: 2 replies')); - L.push(os(a,'slack-attachment-header','Attachment')+' '+os(a,'slack-attachment-field-title','Field:')+' val '+os(a,'slack-message-attachment-preview-header-face','Preview')+' '+os(a,'slack-preview-face','snippet')+os(a,'slack-attachment-pad',' | ')+os(a,'slack-attachment-footer','footer')); - L.push(os(a,'slack-block-highlight-source-overlay-face',' highlighted source block ')); - L.push('Actions: '+os(a,'slack-message-action-face','Edit')+' '+os(a,'slack-message-action-primary-face','Approve')+' '+os(a,'slack-message-action-danger-face','Delete')); - L.push('Blocks: '+os(a,'slack-button-block-element-face','[Button]')+os(a,'slack-button-primary-block-element-face','[Primary]')+os(a,'slack-button-danger-block-element-face','[Danger]')+os(a,'slack-select-block-element-face','[Select v]')+os(a,'slack-overflow-block-element-face','[...]')+os(a,'slack-date-picker-block-element-face','[Date]')); - L.push('Dialog: '+os(a,'slack-dialog-title-face','Title')+' '+os(a,'slack-dialog-element-label-face','Label')+' '+os(a,'slack-dialog-element-hint-face','(hint)')+' '+os(a,'slack-dialog-element-placeholder-face','placeholder')+' '+os(a,'slack-dialog-element-error-face','error')+' '+os(a,'slack-dialog-select-element-input-face','[input v]')+' '+os(a,'slack-dialog-submit-button-face','[Submit]')+os(a,'slack-dialog-cancel-button-face','[Cancel]')); - L.push('Users: '+os(a,'slack-user-active-face','alice (active)')+' '+os(a,'slack-user-dnd-face','bob (dnd)')+' '+os(a,'slack-profile-image-face','[img]')+' '+os(a,'slack-user-profile-header-face','Profile')+' '+os(a,'slack-user-profile-property-name-face','Title:')+' Dev'); - L.push('Search: '+os(a,'slack-search-result-message-header-face','#general')+' '+os(a,'slack-search-result-message-username-face','craig')); - L.push('Modeline: '+os(a,'slack-modeline-has-unreads-face','* unreads')+' '+os(a,'slack-modeline-channel-has-unreads-face','#ch')+' '+os(a,'slack-modeline-thread-has-unreads-face','thread')); - return previewLines(L);} -function renderTelegaPreview(){const a='telega',L=[]; - L.push(os(a,'telega-root-heading','Telegram')+' '+os(a,'telega-tracking','[tracking]')+' '+os(a,'telega-unread-unmuted-modeline','5 unread')); - L.push(os(a,'telega-has-chatbuf-brackets','[')+os(a,'telega-username','Christine')+os(a,'telega-has-chatbuf-brackets',']')+' '+os(a,'telega-user-online-status','online')+' '+os(a,'telega-unmuted-count','3')+' '+os(a,'telega-mention-count','@2')+os(a,'telega-delim-face',' | ')+os(a,'telega-secret-title','Secret')+' '+os(a,'telega-muted-count','muted')); - L.push(os(a,'telega-username','Bob')+' '+os(a,'telega-user-non-online-status','last seen recently')+' '+os(a,'telega-contact-birthdays-today','birthday today')+' '+os(a,'telega-shadow','shadow')+' '+os(a,'telega-link','link')+' '+os(a,'telega-blue','blue')+' '+os(a,'telega-red','red')); - L.push(''); - L.push(os(a,'telega-msg-heading','Today')); - L.push(os(a,'telega-msg-user-title','Christine')+' '+os(a,'telega-msg-inline-reply','| reply to Bob')+' '+os(a,'telega-msg-inline-forward','fwd from Carol')+' '+os(a,'telega-msg-inline-other','via bot')); - L.push(' '+os(a,'telega-entity-type-bold','bold')+' '+os(a,'telega-entity-type-italic','italic')+' '+os(a,'telega-entity-type-underline','underline')+' '+os(a,'telega-entity-type-strikethrough','strike')+' '+os(a,'telega-entity-type-code','code')+' '+os(a,'telega-entity-type-spoiler','spoiler')); - L.push(' '+os(a,'telega-entity-type-pre','pre block')+' '+os(a,'telega-entity-type-blockquote','> quote')+' '+os(a,'telega-entity-type-mention','@user')+' '+os(a,'telega-entity-type-hashtag','#tag')+' '+os(a,'telega-entity-type-cashtag','$USD')+' '+os(a,'telega-entity-type-botcommand','/start')+' '+os(a,'telega-entity-type-texturl','link')); - L.push(os(a,'telega-msg-self-title','Me')+' '+os(a,'telega-reaction',':+1: 2')+' '+os(a,'telega-reaction-chosen',':heart: 1')+' '+os(a,'telega-reaction-paid',':star: 5')+' '+os(a,'telega-reaction-paid-chosen',':star: paid')+' '+os(a,'telega-msg-deleted','(deleted)')+' '+os(a,'telega-msg-sponsored','Sponsored')); - L.push(' checklist '+os(a,'telega-checklist-stats-done','2 done')+' / '+os(a,'telega-checklist-stats-todo','3 todo')+' '+os(a,'telega-highlight-text-face','search hit')+' '+os(a,'telega-button-highlight','[active btn]')); - L.push(os(a,'telega-chat-prompt','>')+' '+os(a,'telega-chat-prompt-aux','reply')+' '+os(a,'telega-chat-input-attachment','[photo.jpg]')+' '+os(a,'telega-topic-button','# Topic')+' '+os(a,'telega-filter-active','Main')+' '+os(a,'telega-filter-button-active','[Unread]')+os(a,'telega-filter-button-inactive','[All]')); - L.push('Buttons '+os(a,'telega-box-button','[box]')+os(a,'telega-box-button-active','[on]')+os(a,'telega-box-button-default-active','[def]')+os(a,'telega-box-button-default-passive','[def-]')+os(a,'telega-box-button-primary-active','[pri]')+os(a,'telega-box-button-primary-passive','[pri-]')+os(a,'telega-box-button-success-active','[ok]')+os(a,'telega-box-button-success-passive','[ok-]')); - L.push(' '+os(a,'telega-box-button-danger-active','[del]')+os(a,'telega-box-button-danger-passive','[del-]')+os(a,'telega-box-button-ui-active','[ui]')+os(a,'telega-box-button-ui-passive','[ui-]')+os(a,'telega-box-button2-active','[b2]')+os(a,'telega-box-button2-passive','[b2-]')+os(a,'telega-box-button2-white-foreground','[b2w]')); - L.push('Describe '+os(a,'telega-describe-section-title','Section')+' '+os(a,'telega-describe-subsection-title','Sub')+' '+os(a,'telega-describe-item-title','Item:')+' enckey '+os(a,'telega-enckey-00','00')+os(a,'telega-enckey-01','01')+os(a,'telega-enckey-10','10')+os(a,'telega-enckey-11','11')); - L.push('Palette '+os(a,'telega-palette-builtin-blue','blue')+' '+os(a,'telega-palette-builtin-green','green')+' '+os(a,'telega-palette-builtin-orange','orange')+' '+os(a,'telega-palette-builtin-purple','purple')); - L.push(os(a,'telega-link-preview-sitename','example.com')+' '+os(a,'telega-link-preview-title','Link preview title')); - L.push('Webpage '+os(a,'telega-webpage-title','Title')+' '+os(a,'telega-webpage-subtitle','Subtitle')+' '+os(a,'telega-webpage-header','Header')+' '+os(a,'telega-webpage-subheader','Subheader')+' '+os(a,'telega-webpage-outline','outline')+' '+os(a,'telega-webpage-fixed','fixed')+' '+os(a,'telega-webpage-preformatted','pre')+' '+os(a,'telega-webpage-marked','marked')+' '+os(a,'telega-webpage-strike-through','strike')+' '+os(a,'telega-webpage-chat-link','chat-link')); - return previewLines(L);} -function genericPreview(app){let h='<div style="padding:10px 14px;font:12pt/1.8 monospace">';for(const [face,label] of APPS[app].faces)h+=`<div data-face="${face}" style="${ofs(app,face)}">${esc(label)}</div>`;return h+'</div>';} -// Bespoke split preview: a focused window beside its auto-dimmed twin, both -// showing the language selected at the top of the page (kept in sync via the -// langsel onchange, which re-runs buildPkgPreview). The left pane carries the -// real per-token syntax colors; the right pane shows what auto-dim does -- every -// default/font-lock face remaps to the single `auto-dim-other-buffers' face, so -// the same code collapses to one faded foreground on the dim background. The -// trailing row demonstrates `auto-dim-other-buffers-hide' (org hidden text whose -// foreground matches the background, so it vanishes in a dimmed window). -function renderAutodimPreview(){ - const a='auto-dim-other-buffers'; - const langsel=document.getElementById('langsel'); - const lang=(langsel&&langsel.value)||Object.keys(SAMPLES)[0]; - const lines=(SAMPLES[lang]||[]).slice(0,9); - let lit=''; - for(const line of lines){ - if(!line.length){lit+='\n';continue;} - for(const [k,t] of line)lit+=`<span data-k="${k}" style="${syntaxStyle(k)}">${esc(t)}</span>`; - lit+='\n';} - const dimFg=effFg(pkgEffFg(a,'auto-dim-other-buffers')),dimBg=pkgEffBg(a,'auto-dim-other-buffers')||'#000000'; - let dim=''; - for(const line of lines){ - if(!line.length){dim+='\n';continue;} - for(const [,t] of line)dim+=esc(t); - dim+='\n';} - const hideFg=effFg(pkgEffFg(a,'auto-dim-other-buffers-hide')),hideBg=pkgEffBg(a,'auto-dim-other-buffers-hide')||dimBg; - const foldText='··· folded body (hidden when dimmed) ···'; - const accent=uf('cursor').bg||'#67809c'; - const pane=(label,body,bg,focused)=> - `<div style="flex:1;min-width:20ch;border:${focused?'2px solid '+accent:'1px solid #2a2a2a'};border-radius:4px;overflow:hidden">` - +`<div style="text-align:center;font:bold 10pt monospace;padding:4px;color:${focused?'#cdced1':'#8a8a8a'};background:${focused?'#1a1a1a':'#0a0a0a'};border-bottom:1px solid #2a2a2a">${label}</div>` - +`<div style="padding:10px 12px;font:12pt/1.6 monospace;white-space:pre;background:${bg}">${body}</div></div>`; - const litBody=lit+'\n'+`<span style="color:#5e6770">${esc(foldText)}</span>`; - const dimBody=`<span data-face="auto-dim-other-buffers" style="color:${dimFg}">${dim}</span>\n` - +`<span data-face="auto-dim-other-buffers-hide" style="color:${hideFg};background:${hideBg}">${esc(foldText)}</span>`; - return `<div style="display:flex;gap:12px;padding:12px 16px;background:${MAP['bg']}">` - +pane('normal',litBody,MAP['bg'],true) - +pane('auto-dim',dimBody,dimBg,false) - +`</div>`; + updateLockToggle('pkg');syncExpandAllBtns(); } +// The per-package preview renderers live in previews.js, spliced here so the +// PACKAGE_PREVIEWS registry below can reference them. +PREVIEWS_J const PACKAGE_PREVIEWS={ - autodim:renderAutodimPreview, + autodim:renderAutodimPreview,markdown:renderMarkdownPreview, org:renderOrgPreview,magit:renderMagitPreview,elfeed:renderElfeedPreview,ghostel:renderGhostelPreview, - dashboard:renderDashboardPreview,mu4e:renderMu4ePreview,orgfaces:renderOrgFacesPreview,lsp:renderLspPreview,gitgutter:renderGitGutterPreview, + dashboard:renderDashboardPreview,mu4e:renderMu4ePreview,gnus:renderGnusPreview,orgfaces:renderOrgFacesPreview,lsp:renderLspPreview,gitgutter:renderGitGutterPreview, flycheck:renderFlycheckPreview,dired:renderDiredPreview,dirvish:renderDirvishPreview,calibredb:renderCalibredbPreview, erc:renderErcPreview,orgdrill:renderOrgdrillPreview,orgnoter:renderOrgnoterPreview,signel:renderSignelPreview, - pearl:renderPearlPreview,slack:renderSlackPreview,telega:renderTelegaPreview,shr:renderShrPreview + pearl:renderPearlPreview,slack:renderSlackPreview,telega:renderTelegaPreview,shr:renderShrPreview, + nerdicons:renderNerdIconsPreview }; +// Preview panes for an app. Most apps have a single pane (the dropdown shows its +// name and is disabled). nerd-icons is the one multi-pane app: one pane per font +// size, so the designer can view the icon grid at different sizes — pt because +// Emacs sizes fonts in :height (1/10 pt), so a pane maps to a real buffer size. +const NERD_ICON_SIZES_PT=[10,12,14,16,20,24,32,48]; +const NERD_ICON_DEFAULT_PT=14; +function previewPanes(app){ + // Multi-pane only when nerd-icons actually has a gallery to size. If the gallery + // capture failed (nerd-icons absent, alists changed, empty), the grid renderer + // falls back to the generic preview, so offering size panes would be a lie — the + // dropdown collapses to one pane and is disabled. + if(app==='nerd-icons'&&APPS[app]&&Array.isArray(APPS[app].gallery)&&APPS[app].gallery.length) + return NERD_ICON_SIZES_PT.map(pt=>({label:'nerd-icons — '+pt+' pt',size:pt})); + return [{label:PACKAGE_PREVIEWS[APPS[app].preview]?APPS[app].label:'generic (face names in their own colors)'}]; +} +function defaultPaneIdx(app){ + if(app==='nerd-icons')return Math.max(0,NERD_ICON_SIZES_PT.indexOf(NERD_ICON_DEFAULT_PT)); + return 0; +} +// Per-app selected pane index, so a chosen size survives edits and revisits. +const PREV_PANE={}; +// The ‹ › buttons flanking the preview dropdown (and the Left/Right arrows) step the +// pane by DIR, clamped, and re-render. No-op on a disabled (single-pane) dropdown. +function stepPreviewPane(dir){ + const s=document.getElementById('pkgprevsel');if(!s||s.disabled)return; + const i=stepViewIndex(s.selectedIndex,s.options.length,dir); + if(i!==s.selectedIndex){PREV_PANE[curApp()]=i;buildPkgPreview();} +} function buildPkgPreview(){ const app=curApp(),p=document.getElementById('pkgpreview');if(!p)return; - const renderer=PACKAGE_PREVIEWS[APPS[app].preview]; - p.innerHTML=renderer?renderer():genericPreview(app); + rebuildLocateRegistry(); + const panes=previewPanes(app); + let idx=PREV_PANE[app]; + if(idx==null||idx>=panes.length){idx=defaultPaneIdx(app);PREV_PANE[app]=idx;} + const pane=panes[idx],renderer=PACKAGE_PREVIEWS[APPS[app].preview]; + // A pane carrying a size is a nerd-icons size variant; render the grid at it. + p.innerHTML=pane.size!=null?renderNerdIconsPreview(pane.size):(renderer?renderer():genericPreview(app)); p.style.background=MAP['bg']; - p.onclick=(e)=>{const u=e.target.closest('[data-face]');if(u)flashPkg(u.dataset.face);}; - const lbl=document.getElementById('pkgprevlabel');if(lbl)lbl.textContent=renderer?(APPS[app].label+' preview'):'preview (generic — face names in their own colors)'; + p.onclick=(e)=>locateClick(e,app); + // The pane dropdown: disabled when there's only one pane (it just names the + // preview), enabled when there are several (it selects which one shows). + const sel=document.getElementById('pkgprevsel'); + if(sel){ + sel.innerHTML=panes.map((pn,i)=>`<option value="${i}">${esc(pn.label)}</option>`).join(''); + sel.value=String(idx); + sel.disabled=panes.length<2; + sel.onchange=()=>{PREV_PANE[app]=+sel.value;buildPkgPreview();}; + // Left/Right arrows step the panes when the dropdown is focused (Up/Down already + // do, natively); re-grab + refocus the select, since the rebuild drops focus. + sel.onkeydown=(e)=>{ + if(e.key!=='ArrowLeft'&&e.key!=='ArrowRight')return; + e.preventDefault(); + stepPreviewPane(e.key==='ArrowRight'?1:-1); + const s=document.getElementById('pkgprevsel');if(s)s.focus(); + }; + // The flanking ‹ › buttons follow the dropdown's enabled state. + const pb=document.getElementById('pkgprevprev'),nb=document.getElementById('pkgprevnext'); + if(pb)pb.disabled=panes.length<2; + if(nb)nb.disabled=panes.length<2; + } + // Per-element wayfinding rides each preview span's own hover title (face + value); + // no separate info line. } function resetApp(){const app=curApp();for(const [face,,d] of APPS[app].faces)if(!LOCKED.has('pkg:'+app+':'+face))PKGMAP[app][face]=seedFace(d);pkgChanged();notify('reset editable '+app+' faces to package defaults',false);} -function syncPkgHeight(){const t=document.getElementById('pkgtable'),m=document.getElementById('pkgpreview');if(!t||!m)return;const lb=m.previousElementSibling,lbh=lb?lb.getBoundingClientRect().height+10:30;m.style.height=Math.max(t.getBoundingClientRect().height-lbh,220)+'px';} // --- worst-case readout for the covered overlay faces (spec Phase 4) --------- // Default WCAG target for the worst-case verdict (AA). AAA is selectable. let WORST_TARGET=4.5; @@ -993,38 +662,44 @@ function worstCellHtml(face){ const report=coveredContrastReport(face); if(report===null)return null; if(report.empty)return '<span title="this overlay has no syntax foreground set yet">no fg set</span>'; - return `<span style="color:${ratingColor(report.worst.ratio)}" title="${esc(failureTitle(report)||'all covered text clears '+WORST_TARGET.toFixed(1))}">${report.worst.ratio.toFixed(1)} ${report.worst.verdict}</span>`; + return `<span style="color:${ratingColor(report.worst.ratio)}" title="${esc(failureTitle(report)||'all covered text clears '+WORST_TARGET.toFixed(1))}">${report.worst.ratio.toFixed(1)}</span>`; } // Repaint every covered overlay face (their floors depend on the syntax palette, // so a syntax-color edit has to refresh them even though it doesn't rebuild the table). function repaintCovered(){COVERED_FACES.forEach(f=>{if(UIMAP[f]&&document.getElementById('uicr-'+f))paintUI(f);});} -function paintUI(face){const pv=document.getElementById('uiprev-'+face);if(!pv)return;const o=UIMAP[face];pv.style.color=effFg(o.fg);pv.style.background=effBg(o.bg);pv.style.fontWeight=o.bold?'bold':'normal';pv.style.fontStyle=o.italic?'italic':'normal';pv.style.textDecoration=(o.underline?'underline ':'')+(o.strike?'line-through':'')||'none';pv.style.boxShadow=boxCss(o.box,effBg(o.bg)); +function paintUI(face){const pv=document.getElementById('uiprev-'+face);if(!pv)return;const o=UIMAP[face];pv.style.color=effFg(o.fg);pv.style.background=effBg(o.bg);pv.style.fontWeight=cssWeight(o.weight);pv.style.fontStyle=o.slant||'normal';pv.style.textDecoration=(o.underline?'underline ':'')+(o.strike?'line-through':'')||'none';pv.style.boxShadow=boxCss(o.box,effBg(o.bg)); const report=coveredContrastReport(face); - pv.querySelectorAll('.crerr').forEach(e=>e.remove()); pv.title=''; - if(report&&report.failures&&report.failures.length){ - const badge=document.createElement('span');badge.className='crerr';badge.textContent=report.worst.ratio.toFixed(1)+' FAIL';badge.title=failureTitle(report);pv.title=badge.title;pv.appendChild(badge); - } - const cr=document.getElementById('uicr-'+face);if(cr){cr.title='';if(report!==null){if(report.empty){cr.title='this overlay has no syntax foreground set yet';cr.innerHTML='<span title="this overlay has no syntax foreground set yet">no fg set</span>';}else{const title=failureTitle(report)||'all covered text clears '+WORST_TARGET.toFixed(1);cr.title=title;cr.innerHTML=`<span style="color:${ratingColor(report.worst.ratio)}" title="${esc(title)}">${report.worst.ratio.toFixed(1)} ${report.worst.verdict}</span>`;}}else{const efg=effFg(o.fg),ebg=effBg(o.bg),r=contrast(efg,ebg);cr.innerHTML=crHtml(r);}}} + const cr=document.getElementById('uicr-'+face);if(cr){cr.title='';const wc=worstCellHtml(face);if(wc!==null){cr.title=report.empty?'this overlay has no syntax foreground set yet':(failureTitle(report)||'all covered text clears '+WORST_TARGET.toFixed(1));cr.innerHTML=wc;}else{const efg=effFg(o.fg),ebg=effBg(o.bg),r=contrast(efg,ebg);cr.innerHTML=crHtml(r);}}} function buildUITable(){ const tb=document.getElementById('uibody');tb.innerHTML=''; for(const [face,label,ex] of UI_FACES){ const tr=document.createElement('tr');tr.dataset.face=face; - const c0=document.createElement('td');c0.className='cat';c0.textContent=label;c0.style.cursor='pointer';c0.title='flash this face in the live preview';c0.onclick=()=>flashUiPreview(face); + const exp=mkExpander(UIMAP[face],tableColCount('uitable'),()=>{paintUI(face);buildMockFrame();},{expandKey:face,showInheritHeight:true,inheritOptions:[''].concat(BASE_INHERITS),defaultHex:effFg(UIMAP[face].fg),ndCheck:()=>overflowNonDefault(UIMAP[face],DEFAULT_UIMAP[face],true)}); + exp.detail.dataset.detailFor=face; + const c0=document.createElement('td');c0.className='cat';c0.title=composeHoverTitle(FACE_DOCS[face],c0.title);c0.appendChild(exp.btn); + const c0lbl=document.createElement('span');c0lbl.textContent=' '+label;c0lbl.style.cursor='pointer';c0lbl.title='flash this face in the live preview';c0lbl.onclick=()=>flashUiPreview(face);c0.appendChild(c0lbl); + // Emacs draws the cursor as a rectangle: its fg colors the glyph sitting on + // it and its bg is the cursor color, but weight/slant/underline/strike and + // box are no-ops on it. Show only fg+bg for the cursor row; mute the rest. + const cursorOnly=(face==='cursor'); + const naCell=t=>{const s=document.createElement('span');s.textContent='—';s.style.opacity='0.4';s.title=t;return s;}; const fgSel=uiSelect(face,'fg'),bgSel=uiSelect(face,'bg'); const cF=document.createElement('td');cF.appendChild(fgSel); const cB=document.createElement('td');cB.appendChild(bgSel); const cS=document.createElement('td'); - const stBtns=mkStyleButtons(at=>UIMAP[face][at],at=>{UIMAP[face][at]=!UIMAP[face][at];paintUI(face);buildMockFrame();}); - const uiCluster=document.createElement('div');uiCluster.className='stylecluster';stBtns.forEach(b=>uiCluster.appendChild(b));cS.appendChild(uiCluster); + const stCtls=cursorOnly?[]:mkStyleControls(UIMAP[face],()=>{paintUI(face);buildMockFrame();},{defaultHex:effFg(UIMAP[face].fg)}); + if(cursorOnly){cS.appendChild(naCell('Emacs ignores weight/slant/underline/strike on the cursor face'));} + else{const uiCluster=document.createElement('div');uiCluster.className='stylecluster';stCtls.forEach(c=>uiCluster.appendChild(c));cS.appendChild(uiCluster);} const cC=document.createElement('td');cC.id='uicr-'+face;cC.style.whiteSpace='nowrap';cC.style.fontSize='10pt'; const cP=document.createElement('td');cP.className='ex';cP.id='uiprev-'+face;cP.textContent=ex;cP.style.padding='4px 10px';cP.style.borderRadius='4px'; - const cX=document.createElement('td');const boxCtl=mkBoxControl(()=>UIMAP[face].box,b=>{UIMAP[face].box=b;paintUI(face);buildMockFrame();},{compact:true});cX.appendChild(boxCtl); - const cL=mkLockCell('ui:'+face,[fgSel,bgSel,...stBtns,boxCtl]); - tr.appendChild(c0);tr.appendChild(cL);tr.appendChild(cF);tr.appendChild(cB);tr.appendChild(cS);tr.appendChild(cC);tr.appendChild(cP);tr.appendChild(cX);tb.appendChild(tr);paintUI(face); + const cX=document.createElement('td');const boxCtl=cursorOnly?null:mkBoxControl(()=>UIMAP[face].box,b=>{UIMAP[face].box=b;paintUI(face);buildMockFrame();},{compact:true}); + if(cursorOnly){cX.appendChild(naCell('Emacs ignores the box attribute on the cursor face'));}else{cX.appendChild(boxCtl);} + const cL=mkLockCell('ui:'+face,cursorOnly?[fgSel,bgSel,...exp.locks]:[fgSel,bgSel,...stCtls,boxCtl,...exp.locks]); + tr.appendChild(cL);tr.appendChild(c0);tr.appendChild(cF);tr.appendChild(cB);tr.appendChild(cS);tr.appendChild(cX);tr.appendChild(cC);tr.appendChild(cP);tb.appendChild(tr);tb.appendChild(exp.detail);paintUI(face); } applyTableSort('uibody'); - updateLockToggle('ui'); + updateLockToggle('ui');syncExpandAllBtns(); } // Generic header-click sort, shared by all three tables. Reads a swatch // dropdown's value, a select value, a numeric input, or cell text (numeric when @@ -1034,13 +709,20 @@ function buildUITable(){ let tableSort={}; function cellVal(td){if(!td)return '';const dd=td.querySelector('.cdd');if(dd)return (dd.dataset.val||'').toLowerCase();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:(x<y?-1:x>y?1:0))*dir;});r.forEach(x=>tb.appendChild(x));} +function applyTableSort(tbId){const s=tableSort[tbId];if(!s)return;const tb=document.getElementById(tbId);if(!tb)return;const dir=s.asc?1:-1; + // Sort only the main rows; each expander detail row rides along right after its + // parent (matched by data-detail-for) so a sort never separates the pair. + const details={};[...tb.rows].forEach(x=>{if(x.classList.contains('detailrow'))details[x.dataset.detailFor]=x;}); + const mains=[...tb.rows].filter(x=>!x.classList.contains('detailrow')); + mains.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:(x<y?-1:x>y?1:0))*dir;}); + mains.forEach(x=>{tb.appendChild(x);const key=x.dataset.face||x.dataset.kind;if(key&&details[key])tb.appendChild(details[key]);});} function initApp(){ + paletteShowFull=false; // open collapsed to base colors; the arrow expands the spans buildLangSel();buildViewSel();renderPalette();rebuildColorTables();renderCode();applyGround(); initGeneratorControls(); - updateTitle();initPicker();buildPkgPreview();syncMockHeight();syncPkgHeight(); + updateTitle();initPicker();buildPkgPreview();syncPaneHeight('uitable','mockframe');syncPaneHeight('pkgtable','pkgpreview'); onViewChange(); } initApp(); -addEventListener('resize',()=>{syncMockHeight();syncPkgHeight();}); +addEventListener('resize',()=>{syncPaneHeight('uitable','mockframe');syncPaneHeight('pkgtable','pkgpreview');}); BROWSER_GATES_J |
