diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-18 22:06:53 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-18 22:06:53 -0500 |
| commit | e110f7afac89322a2af4f3c4ebafe303be044cc2 (patch) | |
| tree | f2b24d43f3c38044bd6127e11be43b157a7db0e5 /scripts/theme-studio/app-core.js | |
| parent | 64153c8d995f1603986f3b44ccbdf9ddb21dfd55 (diff) | |
| download | dotemacs-e110f7afac89322a2af4f3c4ebafe303be044cc2.tar.gz dotemacs-e110f7afac89322a2af4f3c4ebafe303be044cc2.zip | |
refactor(theme-studio): cut the face model over to weight/slant/objects
I replaced the legacy bold/italic/underline/strike booleans with the final model shape across both sides of the tool. weight (light/normal/medium/semibold/bold/heavy) and slant (normal/italic/oblique) replace the bold/italic flags, underline becomes {style: line|wave, color}, strike becomes {color}, and null means unset.
A single migration converts a legacy face on the way in, mirrored as migrateLegacyFace in app-core.js and migrate_legacy in face_specs.py so the JS and Python models can't drift. It runs on import (applyImported, mergePackagesInto) and on every seed that face_spec touches. The captured-snapshot seed (default_faces.seed) narrows the same way it did before. Only bold and italic survive, as weight "bold" and slant "italic", so the generated themes stay byte-identical.
The B/I/U/S toggle buttons keep working through a transitional bridge (legacyStyleOn / toggleLegacyStyle). The weight/slant dropdowns and underline/strike controls that replace them land next. The live previews read the new shape, with a weight name mapped to a numeric CSS font-weight.
The cutover is proven emit-neutral two ways. An ERT test asserts the migrated shapes emit the same attributes as the legacy booleans, and deep-migrating every face in dupre, distinguished, sterling, now, theme, and WIP then running build-theme yields byte-identical output. Full suite green: Python 59, Node 200, ERT 41, plus the browser hash gates.
Diffstat (limited to 'scripts/theme-studio/app-core.js')
| -rw-r--r-- | scripts/theme-studio/app-core.js | 41 |
1 files changed, 36 insertions, 5 deletions
diff --git a/scripts/theme-studio/app-core.js b/scripts/theme-studio/app-core.js index 10fa0c570..f9c752bd0 100644 --- a/scripts/theme-studio/app-core.js +++ b/scripts/theme-studio/app-core.js @@ -14,17 +14,48 @@ import { oklch2hex, srgb2oklab, oklab2oklch, oklab2lrgb, lrgb2hex, inGamut, cont // Resolve a palette name (or a raw #hex) to a hex; null when the name is unknown. function nameToHex(n,palette){if(!n)return null;if(/^#/.test(n))return n;const p=palette.find(p=>p[1]===n);return p?p[0]:null;} +// Convert a face dict's legacy boolean style fields to the new shape: bold -> +// weight "bold", italic -> slant "italic", underline true -> {style:line,color}, +// strike true -> {color}. An explicit weight/slant already set wins over the +// legacy flag. Faces already in the new shape pass through, so this is safe on +// any input. Mirrors migrate_legacy in face_specs.py; keep the two in step. +function migrateLegacyFace(d){ + const out=Object.assign({},d||{}); + if('bold' in out){const b=out.bold;delete out.bold;if(b&&out.weight==null)out.weight='bold';} + 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;} + return out; +} + function normalizePkgFace(d,source,palette){ - d=d||{}; + d=migrateLegacyFace(d||{}); const resolve=(v)=>palette?nameToHex(v,palette):v; - return {fg:resolve(d.fg)??null,bg:resolve(d.bg)??null,'distant-fg':resolve(d['distant-fg'])??null,family:d.family??null,bold:!!d.bold,italic:!!d.italic,underline:!!d.underline,strike:!!d.strike,overline:d.overline??null,inherit:d.inherit??null,height:d.height||1,box:d.box??null,inverse:!!d.inverse,extend:!!d.extend,source:source||d.source||'user'}; + return {fg:resolve(d.fg)??null,bg:resolve(d.bg)??null,'distant-fg':resolve(d['distant-fg'])??null,family:d.family??null,weight:d.weight??null,slant:d.slant??null,underline:d.underline??null,strike:d.strike??null,overline:d.overline??null,inherit:d.inherit??null,height:d.height||1,box:d.box??null,inverse:!!d.inverse,extend:!!d.extend,source:source||d.source||'user'}; +} + +// Transitional bridge for the legacy B/I/U/S toggle buttons (mkStyleButtons), +// which the weight/slant dropdowns and underline/strike controls replace next. +// The button reads on/off and flips a single attribute on the new-shape face. +function legacyStyleOn(f,attr){ + if(attr==='bold')return f.weight==='bold'; + if(attr==='italic')return f.slant==='italic'; + if(attr==='underline')return !!f.underline; + if(attr==='strike')return !!f.strike; + return false; +} +function toggleLegacyStyle(f,attr){ + if(attr==='bold')f.weight=f.weight==='bold'?null:'bold'; + else if(attr==='italic')f.slant=f.slant==='italic'?null:'italic'; + else if(attr==='underline')f.underline=f.underline?null:{style:'line',color:null}; + else if(attr==='strike')f.strike=f.strike?null:{color:null}; } // Seed the package-face map from the app inventory's per-face defaults. function buildPkgmap(apps,palette){const m={};for(const app in apps){m[app]={};for(const row of apps[app].faces){m[app][row[0]]=normalizePkgFace(row[2],'default',palette);}}return m;} // The package faces worth exporting (anything seeded or user-touched), trimmed. -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={fg:f.fg,bg:f.bg,bold:f.bold,italic:f.italic,underline:!!f.underline,strike:!!f.strike,inherit:f.inherit,source:f.source};if(f['distant-fg'])o['distant-fg']=f['distant-fg'];if(f.family)o.family=f.family;if(f.overline)o.overline=f.overline;if(f.inverse)o.inverse=true;if(f.extend)o.extend=true;if(f.height&&f.height!==1)o.height=f.height;if(f.box)o.box=f.box;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={fg:f.fg,bg:f.bg,inherit:f.inherit,source:f.source};if(f.weight)o.weight=f.weight;if(f.slant)o.slant=f.slant;if(f.underline)o.underline=f.underline;if(f.strike)o.strike=f.strike;if(f['distant-fg'])o['distant-fg']=f['distant-fg'];if(f.family)o.family=f.family;if(f.overline)o.overline=f.overline;if(f.inverse)o.inverse=true;if(f.extend)o.extend=true;if(f.height&&f.height!==1)o.height=f.height;if(f.box)o.box=f.box;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');}}} @@ -436,11 +467,11 @@ function faceBoxNonDefaults(cur,def){ return { fg: !eq(cur.fg,def.fg), bg: !eq(cur.bg,def.bg), - style: ['bold','italic','underline','strike'].some(a=>!!cur[a]!==!!def[a]), + style: ['weight','slant','underline','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, 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 }; +export { nameToHex, migrateLegacyFace, legacyStyleOn, toggleLegacyStyle, 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 }; |
