aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rw-r--r--scripts/theme-studio/app-core.js22
-rw-r--r--scripts/theme-studio/app.js27
-rw-r--r--scripts/theme-studio/browser-gates.js20
-rw-r--r--scripts/theme-studio/styles.css1
-rw-r--r--scripts/theme-studio/test-app-core.mjs31
-rw-r--r--scripts/theme-studio/theme-studio.html68
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+'"]');