aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio/controls.js
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-02 23:40:16 -0400
committerCraig Jennings <c@cjennings.net>2026-07-02 23:40:16 -0400
commit3581c7d1c05eb514aa5462b1142605541fb64d9e (patch)
treeb7a10e505c0bb53c67b5c82e6245c000c563c54b /scripts/theme-studio/controls.js
parent630cfddc7060c7019815f8e82f87fb629aefebfa (diff)
downloaddotemacs-3581c7d1c05eb514aa5462b1142605541fb64d9e.tar.gz
dotemacs-3581c7d1c05eb514aa5462b1142605541fb64d9e.zip
feat(theme-studio): inline height control on the exposed face rows
A new size column in the UI and package tables carries one numeric field plus an abs/rel toggle, exposed per the editable-height spec: chrome faces (mode-line family, line-number family, and header-line/tab-bar/tab-line when they arrive) default to absolute 1/10pt entry with a computed pt hint; the seeded heading faces (org-level-*, document title/info, agenda structure/dates, shr headings, and friends) default to a relative multiplier. Any other face carrying a live height exposes the control dynamically; the long tail gets none. Absolute entry takes a positive integer only; relative entry clamps into the 0.1-2.0 range the old field used; garbage never reaches the model. The toggle writes heightMode explicitly and clears the number on a flip, since 130 tenth-points and 1.3x mean different things. The kind-unaware height field in the row expander is retired, and a non-default height now marks the size cell instead of the expander toggle. The seeded set is named statically in app-core.js because the per-row default comes from the captured Emacs snapshot, which carries no heights for those faces. The #preview screenshot hash now accepts @ui/@code view keys so the harness can shoot the UI table.
Diffstat (limited to 'scripts/theme-studio/controls.js')
-rw-r--r--scripts/theme-studio/controls.js65
1 files changed, 58 insertions, 7 deletions
diff --git a/scripts/theme-studio/controls.js b/scripts/theme-studio/controls.js
index e98a69a5..84b25c79 100644
--- a/scripts/theme-studio/controls.js
+++ b/scripts/theme-studio/controls.js
@@ -146,10 +146,12 @@ 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.
+// overline, inverse, extend, and (for ui/syntax, where inherit has no inline
+// column) inherit. Height is not an overflow attribute: exposed faces edit it
+// in their row's size cell (mkHeightControl); the long tail has no height
+// control at all. 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.
+// disable them. opts.inheritOptions and opts.showInherit gate the inherit select.
// 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={
@@ -159,8 +161,7 @@ const DETAIL_HOVERS={
'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)'
+ 'inherit':'base face this one inherits unset attributes from (Emacs :inherit)'
};
function mkDetailEditor(face,onChange,opts={}){
const wrap=document.createElement('div');wrap.className='detailedit';const locks=[];
@@ -173,13 +174,63 @@ function mkDetailEditor(face,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){
+ if(opts.showInherit){
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};}
+
+// The per-row height control (editable-height spec): one numeric field plus an
+// abs/rel toggle, rendered only on exposed rows (heightControlKind decides).
+// The toggle writes heightMode explicitly -- the stored kind is what survives
+// the JSON round-trip, since the number type can't (2.0 saves as 2). Flipping
+// the kind clears the value: 130 tenth-pts and 1.3x mean different things, so
+// no silent conversion. Absolute entries render a computed pt hint beside the
+// field. Returns the element plus the controls for the row's lock cell.
+function mkHeightControl(face,kindDefault,onChange){
+ const wrap=document.createElement('span');wrap.className='heightctl';
+ const inp=document.createElement('input');inp.type='text';inp.className='hstep hval';
+ const tog=document.createElement('button');tog.className='chip htog';
+ const hint=document.createElement('span');hint.className='pthint';
+ const kind=()=>face.heightMode||kindDefault;
+ const paint=()=>{
+ const k=kind();
+ tog.textContent=k==='abs'?'pt':'x';
+ tog.title=k==='abs'
+ ?'absolute height in 1/10 pt, what Emacs stores (click to switch to a relative multiplier)'
+ :'relative multiplier of the inherited height (click to switch to an absolute 1/10 pt value)';
+ inp.value=(typeof face.height==='number'&&face.height!==1)?(''+face.height):'';
+ // no example placeholder: a dim number in a numeric column reads as a set
+ // value; the toggle chip and the titles carry the unit instead
+ inp.placeholder='';
+ inp.title=k==='abs'?'positive whole number of 1/10 pt (130 = 13pt)':'positive multiplier, '+HEIGHT_MIN+'-'+HEIGHT_MAX;
+ hint.textContent=k==='abs'?ptHint(face.height):'';
+ };
+ inp.onchange=()=>{
+ const v=parseHeightEntry(kind(),inp.value);
+ if(v===undefined){
+ notify(kind()==='abs'?'height must be a positive whole number of 1/10 pt (e.g. 130)':'height must be a positive number (e.g. 1.2)',true);
+ paint();return;
+ }
+ if(v===null){face.height=null;face.heightMode=null;}
+ else{
+ if(kind()==='rel'&&parseFloat(inp.value)!==v)notify('height clamped to '+v+' (allowed '+HEIGHT_MIN+'-'+HEIGHT_MAX+')',false);
+ face.height=v;face.heightMode=kind();
+ }
+ paint();onChange();
+ };
+ tog.onclick=()=>{
+ const next=kind()==='abs'?'rel':'abs';
+ face.heightMode=next;
+ if(face.height!=null&&face.height!==1)face.height=null;
+ paint();onChange();
+ };
+ const row=document.createElement('span');row.className='hrow';row.append(inp,tog);
+ wrap.append(row,hint);
+ paint();
+ return {el:wrap,controls:[inp,tog]};
+}
// 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