aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio/theme-studio.html
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/theme-studio/theme-studio.html')
-rw-r--r--scripts/theme-studio/theme-studio.html88
1 files changed, 49 insertions, 39 deletions
diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html
index 7c077610d..7f5727cef 100644
--- a/scripts/theme-studio/theme-studio.html
+++ b/scripts/theme-studio/theme-studio.html
@@ -41,8 +41,11 @@
select.navsel,select.navsel option{background:#1f1c19;color:#e8bd30}
/* Prev/next arrows flanking the view dropdown: step the selection without reopening it.
Scoped under .pkgbar to outweigh the generic `.pkgbar button` rule above. */
- .pkgbar .viewnav{appearance:none;border:1px solid #00000060;border-radius:5px;background:#1f1c19;color:#e8bd30;font:bold 16px monospace;width:26px;height:30px;padding:0;margin:0;cursor:pointer;vertical-align:middle}
- .pkgbar .viewnav:hover{border-color:#e8bd30}
+ .pkgbar .viewnav,.langbar .viewnav{appearance:none;border:1px solid #00000060;border-radius:5px;background:#1f1c19;color:#e8bd30;font:bold 16px monospace;width:26px;height:30px;padding:0;margin:0;cursor:pointer;vertical-align:middle}
+ .pkgbar .viewnav:hover,.langbar .viewnav:hover{border-color:#e8bd30}
+ /* Disabled nav (a single-pane preview): keep the gold, dim it, don't look clickable. */
+ .viewnav:disabled{opacity:0.5;cursor:default}
+ select.navsel:disabled{opacity:0.5}
/* Non-default marker: a small gold corner flag on a per-face setting cell whose
value differs from the face's default. The size box looks identical default
or not, so the flag is the only at-a-glance cue that a value was changed. */
@@ -1206,15 +1209,6 @@ function locateFaceMeta(owner,face,registry){
return e||{owner,face,unassigned:true};
}
-// The owner-aware membership check the preview gate calls: the entry's attributes
-// when (owner, face) is a known face of that owner, null when it isn't (a bad
-// owner is rejected). A known face with no non-default attributes returns {} --
-// still truthy, so membership reads cleanly off the result.
-function previewFaceAttrs(owner,face,registry){
- const e=registry&&registry[locateKey(owner,face)];
- return e?e.attrs:null;
-}
-
// Clickable predicate: an element is on-pane only when its owner is the pane being
// viewed. Recomputed from the current view at render time (never stored in the
// registry), since switching panes changes clickability but not ownership.
@@ -1261,15 +1255,6 @@ function formatLocateTitle(meta){
}
return parts.concat(locateAttrsList(meta.attrs)).join(', ');
}
-
-// The immediate-wayfinding info line shown in the preview-label area on hover:
-// "section > face — value" (effective fg, plus bg when set). An unassigned meta
-// reads "<face> — unassigned". Terser than the title; the title is the full record.
-function locateInfoLine(meta){
- if(!meta||meta.unassigned)return (meta&&meta.face?meta.face:'')+' — unassigned';
- const val=meta.value.fg+(meta.value.bg?' / '+meta.value.bg:'');
- return meta.section+' > '+meta.face+' — '+val;
-}
// Pure color/UI-boundary helpers (normHex, ratingColor, textOn), inlined from
// app-util.js. textOn uses rl from the colormath core above.
// Pure color/UI-boundary helpers: hex-input parsing, the contrast-rating status
@@ -1349,8 +1334,7 @@ function generatorHues(baseHue,scheme,count,rng){
const offsets=[0,120,240,30,150,270,60,180,300,90,210,330];
return offsets.slice(0,n).map(o=>(b+o)%360);
}
- if(scheme==='manual')return Array.from({length:n},(_,i)=>(b+(i*360)/n)%360);
- return Array.from({length:n},(_,i)=>(b+(i*360)/n)%360);
+ return Array.from({length:n},(_,i)=>(b+(i*360)/n)%360); // even spread (manual/default/unknown)
}
function generatorChroma(mode){
return mode==='subdued'?0.055:mode==='vivid'?0.13:0.085;
@@ -1735,6 +1719,10 @@ function renderCode(){
cp.onclick=(e)=>{const s=e.target.closest('[data-k]');if(s)flashAssign(s.dataset.k);};
buildMockFrame();
}
+// controls.js -- the custom dropdown / detail-editor / expander control
+// factories, extracted from app.js for navigability. Inlined raw at the
+// CONTROLS_J token: these are hoisting function declarations plus the
+// dropdown popup state, so the token's position preserves execution order.
// 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.
@@ -1748,15 +1736,13 @@ function mkColorDropdown(options,cur,onPick,opts={}){
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)');
+ function paint(){const shown=cur||(opts.defaultHex||''),nm=cur?nameOf(cur):(opts.defaultName||nameOf(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);};
@@ -1942,6 +1928,7 @@ function mkExpander(face,colspan,onChange,opts={}){
setGlyph();syncExpandAllBtns();};
refreshNd();setGlyph();
return {btn,detail,locks};}
+
// 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;
@@ -1971,7 +1958,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();
@@ -2513,7 +2500,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();
@@ -2638,9 +2627,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();
@@ -3278,7 +3267,6 @@ function buildPkgPreview(){
// 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;
@@ -3321,7 +3309,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){
@@ -3330,16 +3318,23 @@ function buildUITable(){
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 stCtls=mkStyleControls(UIMAP[face],()=>{paintUI(face);buildMockFrame();},{defaultHex:effFg(UIMAP[face].fg)});
- const uiCluster=document.createElement('div');uiCluster.className='stylecluster';stCtls.forEach(c=>uiCluster.appendChild(c));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,...stCtls,boxCtl,...exp.locks]);
+ 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');
@@ -3364,11 +3359,11 @@ 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');});
// Shared gate harness. Each call site keeps its literal location.hash==='#NAMEtest'
// check (run-tests.sh greps it); gate() owns the ok/notes/A setup and the verdict
// postamble. Note format standardized to ' fails=note1,note2'.
@@ -3592,6 +3587,21 @@ if(location.hash==='#mocktest')gate('mocktest',A=>withSavedState(['UIMAP','PKGMA
pickEnum(pkgWeight(),'heavy');
A(PKGMAP[app][face].weight==='heavy'&&PKGMAP[app][face].source==='user','pkg weight dropdown writes the model and marks the face edited');
}));
+// Cursor-row gate (open with #cursorrowtest): the cursor face honors only fg
+// (the glyph on it) and bg (the cursor color); weight/slant/underline/strike and
+// box are no-ops, so the row mutes them to a dash while non-cursor rows keep them.
+if(location.hash==='#cursorrowtest')gate('cursorrowtest',A=>{
+ buildUITable();
+ const rows=[...document.querySelectorAll('#uibody tr')];
+ const cur=rows.find(r=>r.dataset.face==='cursor');
+ A(!!cur,'cursor row present');
+ A(!!cur.cells[2].querySelector('.cdd'),'cursor keeps the fg swatch');
+ A(!!cur.cells[3].querySelector('.cdd'),'cursor keeps the bg swatch');
+ A(!cur.cells[4].querySelector('.enumdd')&&cur.cells[4].textContent.includes('—'),'cursor mutes the style controls');
+ A(cur.cells[5].textContent.includes('—'),'cursor mutes the box control');
+ const ml=rows.find(r=>r.dataset.face==='mode-line');
+ A(!!ml.cells[4].querySelector('.enumdd'),'non-cursor rows keep the style controls');
+});
// Palette-generator gate (open with #generatortest): previewing is non-mutating,
// clicking a generated tile loads the existing selector, adding creates a normal
// singleton base column, and appending a preview column commits all span members
@@ -4308,7 +4318,7 @@ if(location.hash==='#pickertest')gate('pickertest',A=>{
// four radio buttons (none / line / pressed / raised); the color swatch shows
// only while a box style is active.
if(location.hash==='#boxtest')gate('boxtest',A=>{
- LOCKED.clear();const f=UI_FACES[0][0];const saveBox=UIMAP[f].box;
+ LOCKED.clear();const f=UI_FACES.map(x=>x[0]).find(x=>x!=='cursor');const saveBox=UIMAP[f].box; // cursor has no box control by design
UIMAP[f].box=null;buildUITable();
const cell=document.querySelector('#uibody tr[data-face="'+f+'"]').cells[5];
A(!!cell.querySelector('.boxcluster'),'box-cluster-present');
@@ -4327,7 +4337,7 @@ if(location.hash==='#boxtest')gate('boxtest',A=>{
// Style-cluster gate (open with #styletest): the style cell holds a weight
// selector, a slant selector, and box-like underline and strike controls.
if(location.hash==='#styletest')gate('styletest',A=>{
- buildUITable();const f=UI_FACES[0][0];
+ buildUITable();const f=UI_FACES.map(x=>x[0]).find(x=>x!=='cursor'); // cursor row has no style cluster by design
const cell=document.querySelector('#uibody tr[data-face="'+f+'"]').cells[4];
const cluster=cell.querySelector('.stylecluster');
A(!!cluster,'style-cluster-present');