diff options
Diffstat (limited to 'scripts/theme-studio/app.js')
| -rw-r--r-- | scripts/theme-studio/app.js | 291 |
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 |
