aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio/app-core.js
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-02 23:01:54 -0400
committerCraig Jennings <c@cjennings.net>2026-07-02 23:01:54 -0400
commit630cfddc7060c7019815f8e82f87fb629aefebfa (patch)
treeb645a1c56616094eca74efdffe499a51250e1bb1 /scripts/theme-studio/app-core.js
parentf9b379ed52e4b5947bb2a2fc8d2c54c872e39791 (diff)
downloaddotemacs-630cfddc7060c7019815f8e82f87fb629aefebfa.tar.gz
dotemacs-630cfddc7060c7019815f8e82f87fb629aefebfa.zip
feat(theme-studio): explicit absolute-vs-relative face height kind
JSON collapses 2.0 to 2 on save, so a height's number type can't say whether it's a fixed 1/10pt value or a relative multiplier. The face model now carries an explicit heightMode field (abs/rel) through seed, save/load, and export. build-theme.el coerces :height from the kind: abs exports an integer, rel a float, so a relative 2.0 renders as 2.0, never 2. Faces saved before the field existed infer the kind once on load (JS: integer to abs, fractional to rel; Python keeps the authored type, so a float 2.0 seed stays relative) and persist it on the next save. The mode-line seed carries abs explicitly, and WIP.json's eight seeded heights are stamped with their kinds. Regenerating the theme from the stamped WIP.json produces an identical WIP-theme.el, so the round-trip holds.
Diffstat (limited to 'scripts/theme-studio/app-core.js')
-rw-r--r--scripts/theme-studio/app-core.js14
1 files changed, 12 insertions, 2 deletions
diff --git a/scripts/theme-studio/app-core.js b/scripts/theme-studio/app-core.js
index 94b5d7ae..d4e9f9e7 100644
--- a/scripts/theme-studio/app-core.js
+++ b/scripts/theme-studio/app-core.js
@@ -25,6 +25,14 @@ function migrateLegacyFace(d){
if('italic' in out){const i=out.italic;delete out.italic;if(i&&out.slant==null)out.slant='italic';}
if('underline' in out){if(out.underline===true)out.underline={style:'line',color:null};else if(out.underline===false)out.underline=null;}
if('strike' in out){if(out.strike===true)out.strike={color:null};else if(out.strike===false)out.strike=null;}
+ // Height-kind migration: a face saved before heightMode existed carries only
+ // the number, so infer the kind once on load -- integer -> absolute 1/10pt,
+ // fractional -> relative multiplier. An explicit heightMode always wins (the
+ // number type can't be trusted: JSON collapses 2.0 to 2). The identity 1 and
+ // non-numbers infer nothing.
+ if(out.heightMode==null&&typeof out.height==='number'&&isFinite(out.height)&&out.height!==1){
+ out.heightMode=Number.isInteger(out.height)?'abs':'rel';
+ }
return out;
}
@@ -71,7 +79,8 @@ function faceCss(face,fg,bg,opts){
// def : value when unset
// resolve : fg/bg/distant-fg run through the palette name->hex resolver
// coerce : 'bool' -> !!v ; 'height' -> v||1 ; default -> v ?? def
-// emit : export rule -- 'always' | 'truthy' | 'non-one' | 'bool'
+// emit : export rule -- 'always' | 'truthy' | 'non-one' | 'bool' |
+// 'with-height' (only alongside a non-default height)
// A hoisted function rather than a const: the inlined page calls normalizePkgFace
// at top level (seedPkgmap) before this point in source order, and a const would
// be in its temporal dead zone there; a function declaration is hoisted.
@@ -87,6 +96,7 @@ function faceAttrs(){return [
{k:'overline', def:null, emit:'truthy'},
{k:'inherit', def:null, emit:'always'},
{k:'height', def:1, coerce:'height', emit:'non-one'},
+ {k:'heightMode', def:null, emit:'with-height'},
{k:'box', def:null, emit:'truthy'},
{k:'inverse', def:false, coerce:'bool', emit:'bool'},
{k:'extend', def:false, coerce:'bool', emit:'bool'},
@@ -110,7 +120,7 @@ function buildPkgmap(apps,palette){const m={};for(const app in apps){m[app]={};f
// The package faces worth exporting (anything seeded or user-touched), trimmed.
// Driven by FACE_ATTRS: each attribute's `emit` rule decides whether it lands.
-function packagesForExport(map){const out={};for(const app in map){const faces={};for(const face in map[app]){const f=map[app][face];if(f.source==='default'||f.source==='user'||f.source==='cleared'){const o={};for(const a of faceAttrs()){const v=f[a.k];if(a.emit==='always')o[a.k]=v;else if(a.emit==='truthy'){if(v)o[a.k]=v;}else if(a.emit==='non-one'){if(v&&v!==1)o[a.k]=v;}else if(a.emit==='bool'){if(v)o[a.k]=true;}}o.source=f.source;faces[face]=o;}}if(Object.keys(faces).length)out[app]=faces;}return out;}
+function packagesForExport(map){const out={};for(const app in map){const faces={};for(const face in map[app]){const f=map[app][face];if(f.source==='default'||f.source==='user'||f.source==='cleared'){const o={};for(const a of faceAttrs()){const v=f[a.k];if(a.emit==='always')o[a.k]=v;else if(a.emit==='truthy'){if(v)o[a.k]=v;}else if(a.emit==='non-one'){if(v&&v!==1)o[a.k]=v;}else if(a.emit==='with-height'){if(v&&f.height&&f.height!==1)o[a.k]=v;}else if(a.emit==='bool'){if(v)o[a.k]=true;}}o.source=f.source;faces[face]=o;}}if(Object.keys(faces).length)out[app]=faces;}return out;}
// Merge an imported package block into a face map, filling missing fields.
function mergePackagesInto(map,pkgs){if(!pkgs)return;for(const app in pkgs){if(!map[app])map[app]={};for(const face in pkgs[app]){const f=pkgs[app][face]||{};map[app][face]=normalizePkgFace(f,f.source||'user');}}}