diff options
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/theme-studio/app-core.js | 22 | ||||
| -rw-r--r-- | scripts/theme-studio/app.js | 27 | ||||
| -rw-r--r-- | scripts/theme-studio/browser-gates.js | 20 | ||||
| -rw-r--r-- | scripts/theme-studio/styles.css | 1 | ||||
| -rw-r--r-- | scripts/theme-studio/test-app-core.mjs | 31 | ||||
| -rw-r--r-- | scripts/theme-studio/theme-studio.html | 68 |
6 files changed, 139 insertions, 30 deletions
diff --git a/scripts/theme-studio/app-core.js b/scripts/theme-studio/app-core.js index 81a667bd1..566e5a69b 100644 --- a/scripts/theme-studio/app-core.js +++ b/scripts/theme-studio/app-core.js @@ -451,11 +451,29 @@ function faceBoxNonDefaults(cur,def){ return { fg: !eq(cur.fg,def.fg), bg: !eq(cur.bg,def.bg), - style: ['weight','slant','underline','strike'].some(a=>JSON.stringify(cur[a]??null)!==JSON.stringify(def[a]??null)), + style: ['weight','slant','strike'].some(a=>JSON.stringify(cur[a]??null)!==JSON.stringify(def[a]??null)), inherit: !eq(cur.inherit,def.inherit), height: (cur.height||1)!==(def.height||1), box: JSON.stringify(cur.box??null)!==JSON.stringify(def.box??null), }; } -export { nameToHex, migrateLegacyFace, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, resolveSyntaxFg, resolveUiAttr, dropdownRowTextColor, paletteOptionList, galleryModel, appViewKeysSorted, faceBoxNonDefaults, stepViewIndex, spanNeighborHex, slugify, fgSetFor, floor, lMax, COVERED_FACES, columnsFromPalette, usedPaletteHexes, paletteUsages, regenColumn, rankByLightness, stepRepointPlan, sortColumns, sortColumnMembers, groundRoleOfEntry, groundColumnMembersFromPalette, clearPalettePlan, deletePaletteColumnPlan, areAllLocked, lockToggleLabel, toggleLockSet }; +// True when the per-row expander hides at least one attribute that differs from +// the face's default, so the collapsed toggle can flag it. Covers exactly the +// attributes the expander holds: distant-fg, family, underline, overline, +// inverse, extend, and (for ui/syntax) inherit + height. The in-row controls +// (fg/bg/weight/slant/strike/box) have their own cell markers and are excluded. +function overflowNonDefault(cur,def,showInheritHeight){ + cur=cur||{}; def=def||{}; + const eq=(a,b)=>JSON.stringify(a??null)===JSON.stringify(b??null); + if(['distant-fg','family','underline','overline'].some(a=>!eq(cur[a],def[a])))return true; + if((!!cur.inverse)!==(!!def.inverse))return true; + if((!!cur.extend)!==(!!def.extend))return true; + if(showInheritHeight){ + if(!eq(cur.inherit,def.inherit))return true; + if((cur.height||1)!==(def.height||1))return true; + } + return false; +} + +export { nameToHex, migrateLegacyFace, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, resolveSyntaxFg, resolveUiAttr, dropdownRowTextColor, paletteOptionList, galleryModel, appViewKeysSorted, faceBoxNonDefaults, overflowNonDefault, stepViewIndex, spanNeighborHex, slugify, fgSetFor, floor, lMax, COVERED_FACES, columnsFromPalette, usedPaletteHexes, paletteUsages, regenColumn, rankByLightness, stepRepointPlan, sortColumns, sortColumnMembers, groundRoleOfEntry, groundColumnMembersFromPalette, clearPalettePlan, deletePaletteColumnPlan, areAllLocked, lockToggleLabel, toggleLockSet }; diff --git a/scripts/theme-studio/app.js b/scripts/theme-studio/app.js index 6cdcd63a6..8e6b01de6 100644 --- a/scripts/theme-studio/app.js +++ b/scripts/theme-studio/app.js @@ -166,12 +166,14 @@ 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=mkEnumSelect(WEIGHT_OPTS,()=>face.weight,v=>{face.weight=v;onChange();},'font weight'); const s=mkEnumSelect(SLANT_OPTS,()=>face.slant,v=>{face.slant=v;onChange();},'font slant'); - const u=mkUnderlineControl(()=>face.underline,v=>{face.underline=v;onChange();},opts); const k=mkStrikeControl(()=>face.strike,v=>{face.strike=v;onChange();},opts); - return [w,s,u,k];} + 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;} @@ -187,6 +189,7 @@ function mkDetailEditor(face,onChange,opts={}){ 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();})); @@ -203,10 +206,20 @@ function mkDetailEditor(face,onChange,opts={}){ // right after the main row. function mkExpander(face,colspan,onChange,opts={}){ const detail=document.createElement('tr');detail.className='detailrow';detail.style.display='none'; - const td=document.createElement('td');td.colSpan=colspan;const {el,locks}=mkDetailEditor(face,onChange,opts);td.appendChild(el);detail.appendChild(td); - const btn=document.createElement('button');btn.className='exptoggle';btn.textContent='⋯';btn.title='more attributes'; + const btn=document.createElement('button');btn.className='exptoggle';btn.textContent='⋯'; + // 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 open=detail.style.display==='none';detail.style.display=open?'':'none';btn.classList.toggle('on',open);}; + refreshNd(); return {btn,detail,locks};} +// 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. @@ -281,7 +294,7 @@ function buildTable(){ 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 exp=mkExpander(syntaxFace(kind),8,()=>{styleEx();renderCode();},{showInheritHeight:true,inheritOptions:[''].concat(BASE_INHERITS),defaultHex:rowFg()}); + const exp=mkExpander(syntaxFace(kind),tableColCount('legtable'),()=>{styleEx();renderCode();},{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.appendChild(exp.btn); @@ -623,7 +636,7 @@ function buildPkgTable(){ 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,9,()=>{f.source='user';pkgChanged();},{defaultHex:effFg(pkgEffFg(app,face))}); + const exp=mkExpander(f,tableColCount('pkgtable'),()=>{f.source='user';pkgChanged();},{defaultHex:effFg(pkgEffFg(app,face)),ndCheck:()=>overflowNonDefault(f,def,false)}); exp.detail.dataset.detailFor=face; const c0=document.createElement('td');c0.className='cat';c0.title=face;c0.appendChild(exp.btn); const c0lbl=document.createElement('span');c0lbl.textContent=' '+label;c0lbl.style.cursor='pointer';c0lbl.onclick=()=>flashPkgPreview(face);c0.appendChild(c0lbl); @@ -1174,7 +1187,7 @@ 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 exp=mkExpander(UIMAP[face],8,()=>{paintUI(face);buildMockFrame();},{showInheritHeight:true,inheritOptions:[''].concat(BASE_INHERITS),defaultHex:effFg(UIMAP[face].fg)}); + const exp=mkExpander(UIMAP[face],tableColCount('uitable'),()=>{paintUI(face);buildMockFrame();},{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.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); diff --git a/scripts/theme-studio/browser-gates.js b/scripts/theme-studio/browser-gates.js index f3a237666..ebd4a3f00 100644 --- a/scripts/theme-studio/browser-gates.js +++ b/scripts/theme-studio/browser-gates.js @@ -159,9 +159,6 @@ if(location.hash==='#mocktest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c A(uiWeight&&uiWeight.value==='','ui weight select starts empty when model is unset'); uiWeight.value='bold';uiWeight.dispatchEvent(new Event('change')); A(UIMAP['region'].weight==='bold','ui weight select writes the model'); - const uiUnder=regionRow.querySelector('.boxctl .boxbtn[data-style="wave"]'); - uiUnder.click(); - A(UIMAP['region'].underline&&UIMAP['region'].underline.style==='wave','ui underline control writes a wavy underline object'); const app=curApp(),face=APPS[app].faces[0][0];PKGMAP[app][face].weight=null;buildPkgTable(); const pkgWeight=()=>document.querySelector('#pkgbody tr[data-face="'+face+'"] select.stylesel'); A(pkgWeight()&&pkgWeight().value==='','pkg weight select starts empty when model is unset'); @@ -843,7 +840,7 @@ if(location.hash==='#styletest'){let ok=true;const notes=[];const A=(c,n)=>{if(! A(sels.length===2,'weight-and-slant-selectors-present'); A(sels[0]&&[...sels[0].options].some(o=>o.value==='semibold'),'weight-selector-offers-the-curated-range'); A(sels[1]&&[...sels[1].options].some(o=>o.value==='oblique'),'slant-selector-offers-oblique'); - A(cluster&&cluster.querySelectorAll('.boxctl').length===2,'underline-and-strike-controls-present'); + A(cluster&&cluster.querySelectorAll('.boxctl').length===1,'strike-control-in-row-underline-moved-to-expander'); document.title='STYLETEST '+(ok?'PASS':'FAIL'); const d=document.createElement('div');d.id='styletest';d.textContent='STYLETEST '+(ok?'PASS':'FAIL')+(notes.length?' fails='+notes.join(','):'');document.body.appendChild(d);} // Expander gate (open with #expandtest): the per-row "more" toggle reveals a @@ -859,10 +856,15 @@ if(location.hash==='#expandtest'){let ok=true;const notes=[];const A=(c,n)=>{if( btn&&btn.click(); A(detail&&detail.style.display!=='none','toggle-reveals-detail-row'); const ed=detail&&detail.querySelector('.detailedit'); - A(ed&&ed.querySelectorAll('.detailfield').length>=5,'detail-editor-has-the-overflow-fields'); + A(ed&&ed.querySelectorAll('.detailfield').length>=6,'detail-editor-has-the-overflow-fields'); // ui faces also expose inherit + height in the expander A(ed&&ed.querySelector('select.detailsel'),'ui-expander-offers-inherit'); A(ed&&ed.querySelector('input.hstep'),'ui-expander-offers-height'); + // underline moved into the expander; its wave style writes a styled object + const uiUnder=ed&&ed.querySelector('.boxctl .boxbtn[data-style="wave"]'); + A(!!uiUnder,'underline-control-in-expander'); + uiUnder&&uiUnder.click(); + A(UIMAP['region'].underline&&UIMAP['region'].underline.style==='wave','underline-control-writes-a-wavy-object'); // family text input writes the model const fam=ed&&ed.querySelector('input.detailinput'); if(fam){fam.value='Iosevka';fam.dispatchEvent(new Event('change'));} @@ -871,6 +873,14 @@ if(location.hash==='#expandtest'){let ok=true;const notes=[];const A=(c,n)=>{if( const inv=ed&&ed.querySelector('input.detailcheck'); if(inv){inv.checked=true;inv.dispatchEvent(new Event('change'));} A(UIMAP['region'].inverse===true,'inverse-checkbox-writes-the-model'); + // a hidden non-default attribute flags the collapsed toggle (reset region to its + // default first, since the edits above left several overflow attrs changed) + UIMAP['region']=JSON.parse(JSON.stringify(DEFAULT_UIMAP['region']));buildUITable(); + const cleanbtn=document.querySelector('#uibody tr[data-face="region"] .exptoggle'); + A(cleanbtn&&!cleanbtn.classList.contains('exp-nd'),'toggle-unflagged-when-overflow-matches-default'); + UIMAP['region']=JSON.parse(JSON.stringify(DEFAULT_UIMAP['region']));UIMAP['region'].overline={color:null};buildUITable(); + const ndbtn=document.querySelector('#uibody tr[data-face="region"] .exptoggle'); + A(ndbtn&&ndbtn.classList.contains('exp-nd'),'collapsed-toggle-flags-a-hidden-non-default-attr'); // package expander omits inherit/height (they have inline columns) buildPkgTable();const pface=APPS[curApp()].faces[0][0]; const pdetail=document.querySelector('#pkgbody tr.detailrow[data-detail-for="'+pface+'"]'); diff --git a/scripts/theme-studio/styles.css b/scripts/theme-studio/styles.css index 794ebd399..de9aa9fd0 100644 --- a/scripts/theme-studio/styles.css +++ b/scripts/theme-studio/styles.css @@ -23,6 +23,7 @@ select.stylesel{width:78px;padding:2px 4px;font:11px monospace;font-weight:normal} .exptoggle{width:18px;height:18px;padding:0;border:1px solid #3a3a3a;border-radius:3px;background:#1f1c19;color:#8a9496;font:12px monospace;line-height:1;cursor:pointer;vertical-align:middle} .exptoggle.on{background:#3a3320;border-color:#e8bd30;color:#e8bd30} + .exptoggle.exp-nd{border-color:#e8bd30;color:#e8bd30} .exptoggle:disabled{opacity:.3;cursor:default} tr.detailrow>td{background:#15120f;border-top:1px solid #2a2a2a;padding:8px 14px} .detailedit{display:flex;flex-wrap:wrap;align-items:center;gap:14px} diff --git a/scripts/theme-studio/test-app-core.mjs b/scripts/theme-studio/test-app-core.mjs index d5f015d93..457f04d17 100644 --- a/scripts/theme-studio/test-app-core.mjs +++ b/scripts/theme-studio/test-app-core.mjs @@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url'; import { nameToHex, migrateLegacyFace, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, resolveSyntaxFg, resolveUiAttr, dropdownRowTextColor, paletteOptionList, spanNeighborHex, slugify, clearPalettePlan, deletePaletteColumnPlan, groundColumnMembersFromPalette, areAllLocked, lockToggleLabel, toggleLockSet, - galleryModel, appViewKeysSorted, faceBoxNonDefaults, stepViewIndex, + galleryModel, appViewKeysSorted, faceBoxNonDefaults, overflowNonDefault, stepViewIndex, } from './app-core.js'; import { planPaletteGenerator, entriesForGeneratedColumn } from './palette-generator-core.js'; import { oklch2hex, deltaE } from './colormath.js'; @@ -957,12 +957,37 @@ test('faceBoxNonDefaults: a set fg over an empty default flags fg', () => { assert.equal(faceBoxNonDefaults({ fg: '#8ea85e' }, {}).fg, true); assert.equal(faceBoxNonDefaults({}, {}).fg, false); }); -test('faceBoxNonDefaults: any style attr differing flags the style box once', () => { +test('faceBoxNonDefaults: an in-row style attr differing flags the style box once', () => { assert.equal(faceBoxNonDefaults({ weight: 'bold' }, { weight: null }).style, true); assert.equal(faceBoxNonDefaults({ slant: 'italic' }, {}).style, true); - assert.equal(faceBoxNonDefaults({ underline: { style: 'line', color: null } }, {}).style, true); + assert.equal(faceBoxNonDefaults({ strike: { color: null } }, {}).style, true); + // underline lives in the expander now, so it does not flag the in-row style box + assert.equal(faceBoxNonDefaults({ underline: { style: 'line', color: null } }, {}).style, false); assert.equal(faceBoxNonDefaults({ weight: 'bold' }, { weight: 'bold' }).style, false); }); + +test('overflowNonDefault: Normal — flags an expander attr that differs from default', () => { + assert.equal(overflowNonDefault({ family: 'Iosevka' }, {}, false), true); + assert.equal(overflowNonDefault({ underline: { style: 'wave', color: null } }, {}, false), true); + assert.equal(overflowNonDefault({ inverse: true }, {}, false), true); + assert.equal(overflowNonDefault({ 'distant-fg': '#222222' }, {}, false), true); +}); + +test('overflowNonDefault: Boundary — matching attrs and in-row attrs do not flag', () => { + // identical overflow attrs -> no flag + const f = { family: 'Iosevka', overline: { color: '#abc' }, inverse: true }; + assert.equal(overflowNonDefault(f, f, false), false); + // weight/slant/strike are in-row, not the expander's concern + assert.equal(overflowNonDefault({ weight: 'bold', slant: 'italic', strike: { color: null } }, {}, false), false); +}); + +test('overflowNonDefault: Boundary — inherit/height only count when shown in the expander', () => { + // packages keep inherit/height inline (showInheritHeight false) -> not flagged here + assert.equal(overflowNonDefault({ inherit: 'shadow', height: 1.4 }, {}, false), false); + // ui/syntax expose them in the expander (showInheritHeight true) -> flagged + assert.equal(overflowNonDefault({ inherit: 'shadow' }, {}, true), true); + assert.equal(overflowNonDefault({ height: 1.4 }, {}, true), true); +}); test('faceBoxNonDefaults: inherit and box differences are flagged', () => { assert.equal(faceBoxNonDefaults({ inherit: 'bold' }, { inherit: null }).inherit, true); assert.equal(faceBoxNonDefaults({ box: { style: 'line' } }, { box: null }).box, true); diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html index aa358e5fe..c00dccb0a 100644 --- a/scripts/theme-studio/theme-studio.html +++ b/scripts/theme-studio/theme-studio.html @@ -25,6 +25,7 @@ select.stylesel{width:78px;padding:2px 4px;font:11px monospace;font-weight:normal} .exptoggle{width:18px;height:18px;padding:0;border:1px solid #3a3a3a;border-radius:3px;background:#1f1c19;color:#8a9496;font:12px monospace;line-height:1;cursor:pointer;vertical-align:middle} .exptoggle.on{background:#3a3320;border-color:#e8bd30;color:#e8bd30} + .exptoggle.exp-nd{border-color:#e8bd30;color:#e8bd30} .exptoggle:disabled{opacity:.3;cursor:default} tr.detailrow>td{background:#15120f;border-top:1px solid #2a2a2a;padding:8px 14px} .detailedit{display:flex;flex-wrap:wrap;align-items:center;gap:14px} @@ -975,12 +976,30 @@ function faceBoxNonDefaults(cur,def){ return { fg: !eq(cur.fg,def.fg), bg: !eq(cur.bg,def.bg), - style: ['weight','slant','underline','strike'].some(a=>JSON.stringify(cur[a]??null)!==JSON.stringify(def[a]??null)), + style: ['weight','slant','strike'].some(a=>JSON.stringify(cur[a]??null)!==JSON.stringify(def[a]??null)), inherit: !eq(cur.inherit,def.inherit), height: (cur.height||1)!==(def.height||1), box: JSON.stringify(cur.box??null)!==JSON.stringify(def.box??null), }; } + +// True when the per-row expander hides at least one attribute that differs from +// the face's default, so the collapsed toggle can flag it. Covers exactly the +// attributes the expander holds: distant-fg, family, underline, overline, +// inverse, extend, and (for ui/syntax) inherit + height. The in-row controls +// (fg/bg/weight/slant/strike/box) have their own cell markers and are excluded. +function overflowNonDefault(cur,def,showInheritHeight){ + cur=cur||{}; def=def||{}; + const eq=(a,b)=>JSON.stringify(a??null)===JSON.stringify(b??null); + if(['distant-fg','family','underline','overline'].some(a=>!eq(cur[a],def[a])))return true; + if((!!cur.inverse)!==(!!def.inverse))return true; + if((!!cur.extend)!==(!!def.extend))return true; + if(showInheritHeight){ + if(!eq(cur.inherit,def.inherit))return true; + if((cur.height||1)!==(def.height||1))return true; + } + return false; +} // 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 @@ -1559,12 +1578,14 @@ 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=mkEnumSelect(WEIGHT_OPTS,()=>face.weight,v=>{face.weight=v;onChange();},'font weight'); const s=mkEnumSelect(SLANT_OPTS,()=>face.slant,v=>{face.slant=v;onChange();},'font slant'); - const u=mkUnderlineControl(()=>face.underline,v=>{face.underline=v;onChange();},opts); const k=mkStrikeControl(()=>face.strike,v=>{face.strike=v;onChange();},opts); - return [w,s,u,k];} + 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;} @@ -1580,6 +1601,7 @@ function mkDetailEditor(face,onChange,opts={}){ 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();})); @@ -1596,10 +1618,20 @@ function mkDetailEditor(face,onChange,opts={}){ // right after the main row. function mkExpander(face,colspan,onChange,opts={}){ const detail=document.createElement('tr');detail.className='detailrow';detail.style.display='none'; - const td=document.createElement('td');td.colSpan=colspan;const {el,locks}=mkDetailEditor(face,onChange,opts);td.appendChild(el);detail.appendChild(td); - const btn=document.createElement('button');btn.className='exptoggle';btn.textContent='⋯';btn.title='more attributes'; + const btn=document.createElement('button');btn.className='exptoggle';btn.textContent='⋯'; + // 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 open=detail.style.display==='none';detail.style.display=open?'':'none';btn.classList.toggle('on',open);}; + refreshNd(); return {btn,detail,locks};} +// 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. @@ -1674,7 +1706,7 @@ function buildTable(){ 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 exp=mkExpander(syntaxFace(kind),8,()=>{styleEx();renderCode();},{showInheritHeight:true,inheritOptions:[''].concat(BASE_INHERITS),defaultHex:rowFg()}); + const exp=mkExpander(syntaxFace(kind),tableColCount('legtable'),()=>{styleEx();renderCode();},{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.appendChild(exp.btn); @@ -2267,7 +2299,7 @@ function buildPkgTable(){ 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,9,()=>{f.source='user';pkgChanged();},{defaultHex:effFg(pkgEffFg(app,face))}); + const exp=mkExpander(f,tableColCount('pkgtable'),()=>{f.source='user';pkgChanged();},{defaultHex:effFg(pkgEffFg(app,face)),ndCheck:()=>overflowNonDefault(f,def,false)}); exp.detail.dataset.detailFor=face; const c0=document.createElement('td');c0.className='cat';c0.title=face;c0.appendChild(exp.btn); const c0lbl=document.createElement('span');c0lbl.textContent=' '+label;c0lbl.style.cursor='pointer';c0lbl.onclick=()=>flashPkgPreview(face);c0.appendChild(c0lbl); @@ -2818,7 +2850,7 @@ 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 exp=mkExpander(UIMAP[face],8,()=>{paintUI(face);buildMockFrame();},{showInheritHeight:true,inheritOptions:[''].concat(BASE_INHERITS),defaultHex:effFg(UIMAP[face].fg)}); + const exp=mkExpander(UIMAP[face],tableColCount('uitable'),()=>{paintUI(face);buildMockFrame();},{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.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); @@ -3022,9 +3054,6 @@ if(location.hash==='#mocktest'){let ok=true;const notes=[];const A=(c,n)=>{if(!c A(uiWeight&&uiWeight.value==='','ui weight select starts empty when model is unset'); uiWeight.value='bold';uiWeight.dispatchEvent(new Event('change')); A(UIMAP['region'].weight==='bold','ui weight select writes the model'); - const uiUnder=regionRow.querySelector('.boxctl .boxbtn[data-style="wave"]'); - uiUnder.click(); - A(UIMAP['region'].underline&&UIMAP['region'].underline.style==='wave','ui underline control writes a wavy underline object'); const app=curApp(),face=APPS[app].faces[0][0];PKGMAP[app][face].weight=null;buildPkgTable(); const pkgWeight=()=>document.querySelector('#pkgbody tr[data-face="'+face+'"] select.stylesel'); A(pkgWeight()&&pkgWeight().value==='','pkg weight select starts empty when model is unset'); @@ -3706,7 +3735,7 @@ if(location.hash==='#styletest'){let ok=true;const notes=[];const A=(c,n)=>{if(! A(sels.length===2,'weight-and-slant-selectors-present'); A(sels[0]&&[...sels[0].options].some(o=>o.value==='semibold'),'weight-selector-offers-the-curated-range'); A(sels[1]&&[...sels[1].options].some(o=>o.value==='oblique'),'slant-selector-offers-oblique'); - A(cluster&&cluster.querySelectorAll('.boxctl').length===2,'underline-and-strike-controls-present'); + A(cluster&&cluster.querySelectorAll('.boxctl').length===1,'strike-control-in-row-underline-moved-to-expander'); document.title='STYLETEST '+(ok?'PASS':'FAIL'); const d=document.createElement('div');d.id='styletest';d.textContent='STYLETEST '+(ok?'PASS':'FAIL')+(notes.length?' fails='+notes.join(','):'');document.body.appendChild(d);} // Expander gate (open with #expandtest): the per-row "more" toggle reveals a @@ -3722,10 +3751,15 @@ if(location.hash==='#expandtest'){let ok=true;const notes=[];const A=(c,n)=>{if( btn&&btn.click(); A(detail&&detail.style.display!=='none','toggle-reveals-detail-row'); const ed=detail&&detail.querySelector('.detailedit'); - A(ed&&ed.querySelectorAll('.detailfield').length>=5,'detail-editor-has-the-overflow-fields'); + A(ed&&ed.querySelectorAll('.detailfield').length>=6,'detail-editor-has-the-overflow-fields'); // ui faces also expose inherit + height in the expander A(ed&&ed.querySelector('select.detailsel'),'ui-expander-offers-inherit'); A(ed&&ed.querySelector('input.hstep'),'ui-expander-offers-height'); + // underline moved into the expander; its wave style writes a styled object + const uiUnder=ed&&ed.querySelector('.boxctl .boxbtn[data-style="wave"]'); + A(!!uiUnder,'underline-control-in-expander'); + uiUnder&&uiUnder.click(); + A(UIMAP['region'].underline&&UIMAP['region'].underline.style==='wave','underline-control-writes-a-wavy-object'); // family text input writes the model const fam=ed&&ed.querySelector('input.detailinput'); if(fam){fam.value='Iosevka';fam.dispatchEvent(new Event('change'));} @@ -3734,6 +3768,14 @@ if(location.hash==='#expandtest'){let ok=true;const notes=[];const A=(c,n)=>{if( const inv=ed&&ed.querySelector('input.detailcheck'); if(inv){inv.checked=true;inv.dispatchEvent(new Event('change'));} A(UIMAP['region'].inverse===true,'inverse-checkbox-writes-the-model'); + // a hidden non-default attribute flags the collapsed toggle (reset region to its + // default first, since the edits above left several overflow attrs changed) + UIMAP['region']=JSON.parse(JSON.stringify(DEFAULT_UIMAP['region']));buildUITable(); + const cleanbtn=document.querySelector('#uibody tr[data-face="region"] .exptoggle'); + A(cleanbtn&&!cleanbtn.classList.contains('exp-nd'),'toggle-unflagged-when-overflow-matches-default'); + UIMAP['region']=JSON.parse(JSON.stringify(DEFAULT_UIMAP['region']));UIMAP['region'].overline={color:null};buildUITable(); + const ndbtn=document.querySelector('#uibody tr[data-face="region"] .exptoggle'); + A(ndbtn&&ndbtn.classList.contains('exp-nd'),'collapsed-toggle-flags-a-hidden-non-default-attr'); // package expander omits inherit/height (they have inline columns) buildPkgTable();const pface=APPS[curApp()].faces[0][0]; const pdetail=document.querySelector('#pkgbody tr.detailrow[data-detail-for="'+pface+'"]'); |
