aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/theme-studio/app.js')
-rw-r--r--scripts/theme-studio/app.js291
1 files changed, 67 insertions, 224 deletions
diff --git a/scripts/theme-studio/app.js b/scripts/theme-studio/app.js
index 0faf9923f..d1aa2eb2c 100644
--- a/scripts/theme-studio/app.js
+++ b/scripts/theme-studio/app.js
@@ -68,213 +68,7 @@ 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;}
-// The in-row style controls, shared by the syntax / UI / package tables: a weight
-// selector, a slant selector, and box-like underline and strike controls. Each
-// edit mutates the face object and calls onChange to repaint. Returns the control
-// elements so the caller lays them out and hands them to mkLockCell.
-const WEIGHT_OPTS=[['light','light'],['normal','normal'],['medium','medium'],['semibold','semibold'],['bold','bold'],['heavy','heavy']];
-const SLANT_OPTS=[['normal','normal'],['italic','italic'],['oblique','oblique']];
-// A compact custom dropdown for an enum attribute (weight / slant), themed like
-// the color dropdown. The trigger shows the current value drawn in its own weight
-// or slant; the popup lists each option drawn with the attribute applied, so the
-// choice previews itself. opts.styleFor(value) returns the preview style props
-// ({fontWeight} / {fontStyle}); opts.placeholder is the unset-state label.
-function mkEnumDropdown(options,get,set,opts={}){
- const t=document.createElement('div');t.className='cdd enumdd';t.tabIndex=0;
- const styleFor=opts.styleFor||(()=>({}));
- const labelOf=v=>{const o=options.find(p=>p[0]===v);return o?o[1]:'';};
- function applyPreview(el,v){el.style.fontWeight='';el.style.fontStyle='';const s=styleFor(v);if(s.fontWeight)el.style.fontWeight=s.fontWeight;if(s.fontStyle)el.style.fontStyle=s.fontStyle;}
- function paint(){const v=get()||'';t.dataset.val=v;t.classList.toggle('is-default',!v);
- t.textContent=v?labelOf(v):(opts.placeholder||'set');applyPreview(t,v);t.title=opts.title||'';}
- paint();
- t.onclick=(e)=>{e.stopPropagation();if(t.dataset.locked==='1')return;if(_ddPop){closeColorDropdown();return;}
- const pop=document.createElement('div');pop.className='cddpop enumpop';const cur=get()||'';
- const pick=v=>{set(v||null);paint();closeColorDropdown();};
- const def=document.createElement('button');def.type='button';
- def.className='enumopt enumdef'+(cur===''?' sel':'');def.textContent='default';
- def.title='clear — use the default';def.onclick=ev=>{ev.stopPropagation();pick('');};pop.appendChild(def);
- for(const [v,label] of options){const b=document.createElement('button');b.type='button';
- b.className='enumopt'+(v===cur?' sel':'');b.textContent=label;applyPreview(b,v);
- b.onclick=ev=>{ev.stopPropagation();pick(v);};pop.appendChild(b);}
- 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';
- _ddPop=pop;};
- t.setValue=()=>paint();t.syncLocked=()=>paint();
- return t;}
-// Underline control: none / line / wave glyph buttons plus a color swatch shown
-// while a style is active. Mirrors mkBoxControl; get()/set() read and write the
-// underline object ({style,color}) or null.
-function mkLineStyleControl(states,get,set,opts={}){const wrap=document.createElement('div');wrap.className='boxctl';
- const cluster=document.createElement('div');cluster.className='boxcluster';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?(opts.toState?opts.toState(v,cur):Object.assign({color:(cur&&cur.color)||null},opts.styled?{style:v}:{})):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(),active=opts.styled?(cur&&cur.style?cur.style:''):(cur?'on':'');
- for(const v in btns)btns[v].classList.toggle('on',v===active);
- dd.style.display=active?'':'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||!active;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;}
-function mkUnderlineControl(get,set,opts={}){
- return mkLineStyleControl([['','no underline',''],['line','underline','_'],['wave','wavy underline','~']],get,set,Object.assign({styled:true},opts));}
-function mkStrikeControl(get,set,opts={}){
- return mkLineStyleControl([['','no strike',''],['on','strike-through','S']],get,set,Object.assign({styled:false},opts));}
-// In-row style controls: weight + slant selectors and a strike control. The
-// underline control lives in the per-row expander (it carries the wave/color
-// detail), keeping the row compact.
-function mkStyleControls(face,onChange,opts={}){
- const w=mkEnumDropdown(WEIGHT_OPTS,()=>face.weight,v=>{face.weight=v;onChange();},{placeholder:'weight',title:'font weight',styleFor:v=>({fontWeight:cssWeight(v)})});
- const s=mkEnumDropdown(SLANT_OPTS,()=>face.slant,v=>{face.slant=v;onChange();},{placeholder:'slant',title:'font slant',styleFor:v=>({fontStyle:v||'normal'})});
- const k=mkStrikeControl(()=>face.strike,v=>{face.strike=v;onChange();},opts);
- return [w,s,k];}
-function mkOverlineControl(get,set,opts={}){
- return mkLineStyleControl([['','no overline',''],['on','overline','O']],get,set,Object.assign({styled:false},opts));}
-function mkCheck(get,set){const c=document.createElement('input');c.type='checkbox';c.className='detailcheck';c.checked=!!get();c.onchange=()=>set(c.checked);return c;}
-// The per-row attribute editor revealed by the expander: distant-fg, family,
-// overline, inverse, extend, and (for ui/syntax, where inherit/height have no
-// inline column) inherit + height. Each control mutates FACE and calls onChange.
-// Returns the element plus the interactive controls so the row's lock cell can
-// disable them. opts.inheritOptions and opts.showInheritHeight gate the last two.
-// Hover help for each expander field, so the detail labels explain themselves the
-// way the table-header labels do. Keyed by the label text passed to add().
-const DETAIL_HOVERS={
- 'distant fg':'foreground swapped in when the text sits on a background too close to its own color to read (Emacs :distant-foreground)',
- 'family':'font family for this face; blank inherits the default (Emacs :family)',
- 'underline':'underline style and color (Emacs :underline)',
- 'overline':'a line drawn above the text (Emacs :overline)',
- 'inverse':'swap the foreground and background (Emacs :inverse-video)',
- 'extend':'extend the background past the end of the line to the window edge (Emacs :extend)',
- 'inherit':'base face this one inherits unset attributes from (Emacs :inherit)',
- 'height':'text size as a scaling factor of the inherited height, 0.1 to 2.0 (Emacs :height)'
-};
-function mkDetailEditor(face,onChange,opts={}){
- const wrap=document.createElement('div');wrap.className='detailedit';const locks=[];
- const add=(label,el)=>{const g=document.createElement('label');g.className='detailfield';g.title=DETAIL_HOVERS[label]||'';const s=document.createElement('span');s.textContent=label;g.append(s,el);wrap.appendChild(g);locks.push(el);};
- const df=mkColorDropdown(ddList(face['distant-fg']||''),face['distant-fg']||'',h=>{face['distant-fg']=h||null;onChange();},{compact:true,defaultHex:opts.defaultHex});
- add('distant fg',df);
- const fam=document.createElement('input');fam.type='text';fam.className='detailinput';fam.placeholder='font family';fam.value=face.family||'';fam.onchange=()=>{face.family=fam.value.trim()||null;onChange();};
- add('family',fam);
- add('underline',mkUnderlineControl(()=>face.underline,v=>{face.underline=v;onChange();},opts));
- add('overline',mkOverlineControl(()=>face.overline,v=>{face.overline=v;onChange();},opts));
- add('inverse',mkCheck(()=>face.inverse,v=>{face.inverse=v;onChange();}));
- add('extend',mkCheck(()=>face.extend,v=>{face.extend=v;onChange();}));
- if(opts.showInheritHeight){
- const isel=document.createElement('select');isel.className='chip detailsel';
- (opts.inheritOptions||['']).forEach(o=>{const op=document.createElement('option');op.value=o;op.textContent=o||'— none —';isel.appendChild(op);});
- isel.value=face.inherit||'';isel.onchange=()=>{face.inherit=isel.value||null;onChange();};add('inherit',isel);
- const hin=document.createElement('input');hin.type='number';hin.min=''+HEIGHT_MIN;hin.max=''+HEIGHT_MAX;hin.step='0.05';hin.className='hstep';hin.value=face.height||1;hin.onchange=()=>{const raw=hin.value,h=clampHeight(raw);face.height=h;hin.value=h==null?1:h;if(h!=null&&parseFloat(raw)!==h)notify('height clamped to '+h+' (allowed '+HEIGHT_MIN+'–'+HEIGHT_MAX+')',false);onChange();};add('height',hin);
- }
- return {el:wrap,locks};}
-// Wire a per-row expander: a toggle button plus a hidden detail row (colspan
-// across the table) holding mkDetailEditor. The caller drops the button into a
-// cell, adds the returned locks to the row's lock cell, and inserts detailRow
-// right after the main row.
-// Which rows have their detail expanded, keyed by the row's element/face key.
-// Held outside the DOM so a table rebuild (a package edit rebuilds the whole
-// table) re-opens the rows that were open, instead of collapsing them under the
-// user — editing a value in an open expander must not close it.
-let EXPANDED=new Set();
-function mkExpander(face,colspan,onChange,opts={}){
- const detail=document.createElement('tr');detail.className='detailrow';detail.style.display='none';
- if(opts.expandKey&&EXPANDED.has(opts.expandKey))detail.style.display='';
- const btn=document.createElement('button');btn.className='exptoggle';
- // The disclosure triangle shows the row's state: ▶ collapsed, ▼ expanded.
- const setGlyph=()=>{const open=detail.style.display!=='none';btn.textContent=open?'▼':'▶';btn.classList.toggle('on',open);};
- // Flag the toggle when collapsed and at least one hidden attribute differs from
- // the default, so a non-default attribute is never invisible. ndCheck re-runs
- // after every edit (for tiers whose onChange does not rebuild the row).
- const ndCheck=opts.ndCheck||(()=>false);
- const refreshNd=()=>{const nd=ndCheck();btn.classList.toggle('exp-nd',nd);btn.title=nd?'more attributes (some differ from default)':'more attributes';};
- const wrapped=()=>{onChange();refreshNd();};
- const td=document.createElement('td');td.colSpan=colspan;const {el,locks}=mkDetailEditor(face,wrapped,opts);td.appendChild(el);detail.appendChild(td);
- btn.onclick=()=>{const willOpen=detail.style.display==='none';detail.style.display=willOpen?'':'none';
- if(opts.expandKey){willOpen?EXPANDED.add(opts.expandKey):EXPANDED.delete(opts.expandKey);}
- setGlyph();syncExpandAllBtns();};
- refreshNd();setGlyph();
- return {btn,detail,locks};}
+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;
@@ -304,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();
@@ -595,7 +389,9 @@ 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;return faceCss(o,fg,bg,{noBg:opts.noBg,boxBg:bg||MAP['bg']});}
-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';}
+// 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();
@@ -720,9 +516,9 @@ function onViewChange(){const s=document.getElementById('viewsel');const v=(s&&s
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();
@@ -768,23 +564,70 @@ const PACKAGE_PREVIEWS={
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;
rebuildLocateRegistry();
- const renderer=PACKAGE_PREVIEWS[APPS[app].preview];
- p.innerHTML=renderer?renderer():genericPreview(app);
+ 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)=>locateClick(e,app);
- const lbl=document.getElementById('pkgprevlabel'),baseLabel=renderer?(APPS[app].label+' preview'):'preview (generic — face names in their own colors)';
- if(lbl)lbl.textContent=baseLabel;
- // Immediate-wayfinding info line: hovering an element shows "section > face —
- // value" in the label area (the element's title is the deterministic fallback);
- // leaving the preview restores the base label.
- p.onmouseover=(e)=>{const u=e.target.closest('[data-owner-app]');if(!u||!lbl)return;lbl.textContent=locateInfoLine(locateFaceMeta(u.dataset.ownerApp,u.dataset.face,LOCATE_REG));};
- p.onmouseleave=()=>{if(lbl)lbl.textContent=baseLabel;};
+ // 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;
@@ -827,7 +670,7 @@ function repaintCovered(){COVERED_FACES.forEach(f=>{if(UIMAP[f]&&document.getEle
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.title='';
- 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)}</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){
@@ -870,9 +713,9 @@ 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