diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-02 23:50:27 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-02 23:50:27 -0400 |
| commit | 0ffd6f5a450e716e7ef3297d4bec2fda36649cdf (patch) | |
| tree | be3eea222da7603fd14a0830488388eaa9198b48 | |
| parent | 3581c7d1c05eb514aa5462b1142605541fb64d9e (diff) | |
| download | dotemacs-0ffd6f5a450e716e7ef3297d4bec2fda36649cdf.tar.gz dotemacs-0ffd6f5a450e716e7ef3297d4bec2fda36649cdf.zip | |
feat(theme-studio): previews render the chosen face height
heightCssValue maps a face's height to CSS from its stored kind: a relative multiplier renders as em, an absolute 1/10pt value as true pt, with legacy objects falling back to integer/fractional inference. uiCss feeds it to the mock editor, so the mode-line, mode-line-inactive, and line-number bars visibly thicken with an absolute height while the buffer text stays put; paintUI scales the UI row's sample text; the package-preview span builder swaps its em-only sizing for the same kind-aware value.
faceCss now accepts a unit-carrying string for fontSize alongside the existing em number.
| -rw-r--r-- | scripts/theme-studio/app-core.js | 13 | ||||
| -rw-r--r-- | scripts/theme-studio/app.js | 4 | ||||
| -rw-r--r-- | scripts/theme-studio/previews.js | 2 | ||||
| -rw-r--r-- | scripts/theme-studio/test-app-core.mjs | 26 | ||||
| -rw-r--r-- | scripts/theme-studio/theme-studio.html | 17 |
5 files changed, 52 insertions, 10 deletions
diff --git a/scripts/theme-studio/app-core.js b/scripts/theme-studio/app-core.js index e8f99835..5deca5c5 100644 --- a/scripts/theme-studio/app-core.js +++ b/scripts/theme-studio/app-core.js @@ -66,7 +66,7 @@ function faceCss(face,fg,bg,opts){ parts.push('font-weight:'+cssWeight(face.weight), 'font-style:'+(face.slant||'normal'), 'text-decoration:'+faceDecoration(face)); - if(opts.fontSize!=null)parts.push('font-size:'+opts.fontSize+'em'); + if(opts.fontSize!=null)parts.push('font-size:'+opts.fontSize+(typeof opts.fontSize==='number'?'em':'')); const bx=boxCss(face.box,opts.boxBg); if(bx)parts.push('box-shadow:'+bx); return parts.join(';'); @@ -600,6 +600,15 @@ function parseHeightEntry(kind,raw){ } // The computed hint beside an absolute entry: 130 -> "= 13.0pt". function ptHint(height){return typeof height==='number'&&isFinite(height)?('= '+(height/10).toFixed(1)+'pt'):'';} +// CSS font-size for a face's height: a relative multiplier renders as em, an +// absolute 1/10pt value as true pt (the preview shows real size), unset or the +// identity 1 as null (inherit). The stored heightMode rules; a legacy object +// without one falls back to integer/fractional inference, same as the loader. +function heightCssValue(f){ + if(!f||typeof f.height!=='number'||!isFinite(f.height)||f.height===1)return null; + const kind=f.heightMode||(Number.isInteger(f.height)?'abs':'rel'); + return kind==='abs'?(f.height/10)+'pt':f.height+'em'; +} // Compose an element-hover tooltip: the face's docstring on top, the existing // hover text (e.g. the bare face name) below it, separated by a blank line. A @@ -756,4 +765,4 @@ function formatLocateTitle(meta){ return parts.concat(locateAttrsList(meta.attrs)).join(', '); } -export { nameToHex, migrateLegacyFace, cssWeight, faceDecoration, boxCss, faceCss, composeHoverTitle, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, resolveSyntaxFg, resolveUiAttr, paletteOptionList, galleryModel, appViewKeysSorted, faceBoxNonDefaults, overflowNonDefault, clampHeight, HEIGHT_MIN, HEIGHT_MAX, isChromeFace, heightControlKind, parseHeightEntry, ptHint, stepViewIndex, spanNeighborHex, slugify, fgSetFor, floor, lMax, COVERED_FACES, columnsFromPalette, usedPaletteHexes, paletteUsages, regenColumn, rankByLightness, stepRepointPlan, sortColumns, sortColumnMembers, groundRoleOfEntry, groundColumnMembersFromPalette, clearPalettePlan, deletePaletteColumnPlan, areAllLocked, lockToggleLabel, toggleLockSet, buildLocateRegistry, locateFaceMeta, formatLocateTitle, isLocateOnPane }; +export { nameToHex, migrateLegacyFace, cssWeight, faceDecoration, boxCss, faceCss, composeHoverTitle, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, resolveSyntaxFg, resolveUiAttr, paletteOptionList, galleryModel, appViewKeysSorted, faceBoxNonDefaults, overflowNonDefault, clampHeight, HEIGHT_MIN, HEIGHT_MAX, isChromeFace, heightControlKind, parseHeightEntry, ptHint, heightCssValue, stepViewIndex, spanNeighborHex, slugify, fgSetFor, floor, lMax, COVERED_FACES, columnsFromPalette, usedPaletteHexes, paletteUsages, regenColumn, rankByLightness, stepRepointPlan, sortColumns, sortColumnMembers, groundRoleOfEntry, groundColumnMembersFromPalette, clearPalettePlan, deletePaletteColumnPlan, areAllLocked, lockToggleLabel, toggleLockSet, buildLocateRegistry, locateFaceMeta, formatLocateTitle, isLocateOnPane }; diff --git a/scripts/theme-studio/app.js b/scripts/theme-studio/app.js index 59b2b5a8..44a01514 100644 --- a/scripts/theme-studio/app.js +++ b/scripts/theme-studio/app.js @@ -388,7 +388,7 @@ function flashUiPreview(f){const sp=document.querySelectorAll(`#mockframe [data- function flashPkg(f){flashRow(document.querySelector(`#pkgbody tr[data-face="${f}"]`));} function flashPkgPreview(f){const sp=document.querySelectorAll(`#pkgpreview [data-face="${f}"]`);if(sp.length){flashEls(sp);return;}const row=document.querySelector(`#pkgbody tr[data-face="${f}"]`);if(row)flashEl(row.querySelector('.cat'));} function mockSpan(k,t){return `<span data-k="${k}" style="${syntaxStyle(k)}">${esc(t)}</span>`;} -function uiCss(o,fgv,bgv,opts={}){const fg=fgv===undefined?effFg(o.fg):fgv,bg=bgv===undefined?o.bg:bgv;return faceCss(o,fg,bg,{noBg:opts.noBg,boxBg:bg||MAP['bg']});} +function uiCss(o,fgv,bgv,opts={}){const fg=fgv===undefined?effFg(o.fg):fgv,bg=bgv===undefined?o.bg:bgv;return faceCss(o,fg,bg,{noBg:opts.noBg,fontSize:heightCssValue(o),boxBg:bg||MAP['bg']});} // Size a preview pane to its faces table, minus the label bar above it. Shared by // the UI mock and the package preview, which differ only in their element IDs. function syncPaneHeight(tableId,paneId){const t=document.getElementById(tableId),m=document.getElementById(paneId);if(!t||!m)return;const lb=m.previousElementSibling,lbh=lb?lb.getBoundingClientRect().height+10:30;m.style.height=Math.max(t.getBoundingClientRect().height-lbh,220)+'px';} @@ -687,7 +687,7 @@ function worstCellHtml(face){ // Repaint every covered overlay face (their floors depend on the syntax palette, // so a syntax-color edit has to refresh them even though it doesn't rebuild the table). function repaintCovered(){COVERED_FACES.forEach(f=>{if(UIMAP[f]&&document.getElementById('uicr-'+f))paintUI(f);});} -function paintUI(face){const pv=document.getElementById('uiprev-'+face);if(!pv)return;const o=UIMAP[face];pv.style.color=effFg(o.fg);pv.style.background=effBg(o.bg);pv.style.fontWeight=cssWeight(o.weight);pv.style.fontStyle=o.slant||'normal';pv.style.textDecoration=(o.underline?'underline ':'')+(o.strike?'line-through':'')||'none';pv.style.boxShadow=boxCss(o.box,effBg(o.bg)); +function paintUI(face){const pv=document.getElementById('uiprev-'+face);if(!pv)return;const o=UIMAP[face];pv.style.color=effFg(o.fg);pv.style.background=effBg(o.bg);pv.style.fontWeight=cssWeight(o.weight);pv.style.fontStyle=o.slant||'normal';pv.style.fontSize=heightCssValue(o)||'';pv.style.textDecoration=(o.underline?'underline ':'')+(o.strike?'line-through':'')||'none';pv.style.boxShadow=boxCss(o.box,effBg(o.bg)); const report=coveredContrastReport(face); pv.title=''; const cr=document.getElementById('uicr-'+face);if(cr){cr.title='';const wc=worstCellHtml(face);if(wc!==null){cr.title=report.empty?'this overlay has no syntax foreground set yet':(failureTitle(report)||'all covered text clears '+WORST_TARGET.toFixed(1));cr.innerHTML=wc;}else{const efg=effFg(o.fg),ebg=effBg(o.bg),r=contrast(efg,ebg);cr.innerHTML=crHtml(r);}}} diff --git a/scripts/theme-studio/previews.js b/scripts/theme-studio/previews.js index 8f8c7698..52a0c640 100644 --- a/scripts/theme-studio/previews.js +++ b/scripts/theme-studio/previews.js @@ -2,7 +2,7 @@ // app.js. Pure preview HTML builders (ofs/os/previewLines + renderXxxPreview); // they reference shared globals (PKGMAP, MAP, faceCss, effFg, ...) and are // inlined into the page's single script element via the PREVIEWS_J token in app.js. -function ofs(app,face){const f=PKGMAP[app][face]||{},fg=effFg(pkgEffFg(app,face)),bg=pkgEffBg(app,face);return faceCss(f,fg,bg,{fontSize:(f.height||1),boxBg:bg||MAP['bg']});} +function ofs(app,face){const f=PKGMAP[app][face]||{},fg=effFg(pkgEffFg(app,face)),bg=pkgEffBg(app,face);return faceCss(f,fg,bg,{fontSize:heightCssValue(f),boxBg:bg||MAP['bg']});} // The CSS for a UI-owned face rendered off any preview surface: effective fg // (floored to the default fg) and bg, following the built-in UI inherit chain so // the rendered color matches what the registry reports. The @ui counterpart to ofs. diff --git a/scripts/theme-studio/test-app-core.mjs b/scripts/theme-studio/test-app-core.mjs index 29c43abd..195846e7 100644 --- a/scripts/theme-studio/test-app-core.mjs +++ b/scripts/theme-studio/test-app-core.mjs @@ -11,7 +11,7 @@ import { clearPalettePlan, deletePaletteColumnPlan, groundColumnMembersFromPalette, areAllLocked, lockToggleLabel, toggleLockSet, galleryModel, appViewKeysSorted, faceBoxNonDefaults, overflowNonDefault, stepViewIndex, cssWeight, faceDecoration, boxCss, faceCss, composeHoverTitle, - clampHeight, HEIGHT_MIN, HEIGHT_MAX, heightControlKind, parseHeightEntry, ptHint, + clampHeight, HEIGHT_MIN, HEIGHT_MAX, heightControlKind, parseHeightEntry, ptHint, heightCssValue, } from './app-core.js'; import { planPaletteGenerator, entriesForGeneratedColumn } from './palette-generator-core.js'; import { oklch2hex, deltaE } from './colormath.js'; @@ -1098,6 +1098,30 @@ test('ptHint: Boundary — no number, no hint', () => { assert.equal(ptHint(null), ''); assert.equal(ptHint(undefined), ''); }); + +test('heightCssValue: Normal — relative renders as em, absolute as true pt', () => { + assert.equal(heightCssValue({ height: 1.3, heightMode: 'rel' }), '1.3em'); + assert.equal(heightCssValue({ height: 130, heightMode: 'abs' }), '13pt'); + // the integral-float case: the stored kind rules, not the number type + assert.equal(heightCssValue({ height: 2, heightMode: 'rel' }), '2em'); +}); + +test('heightCssValue: Normal — a legacy object without a kind infers from the number', () => { + assert.equal(heightCssValue({ height: 1.4 }), '1.4em'); + assert.equal(heightCssValue({ height: 130 }), '13pt'); +}); + +test('heightCssValue: Boundary — unset, identity, or non-number yields null', () => { + assert.equal(heightCssValue({ height: null }), null); + assert.equal(heightCssValue({ height: 1 }), null); + assert.equal(heightCssValue({}), null); + assert.equal(heightCssValue(null), null); +}); + +test('faceCss: Normal — a string fontSize lands verbatim, a number stays em', () => { + assert.ok(faceCss({}, '#111', null, { fontSize: '13pt' }).includes('font-size:13pt')); + assert.ok(faceCss({}, '#111', null, { fontSize: 1.15 }).includes('font-size:1.15em')); +}); 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 ab3a273a..2f74bee9 100644 --- a/scripts/theme-studio/theme-studio.html +++ b/scripts/theme-studio/theme-studio.html @@ -628,7 +628,7 @@ function faceCss(face,fg,bg,opts){ parts.push('font-weight:'+cssWeight(face.weight), 'font-style:'+(face.slant||'normal'), 'text-decoration:'+faceDecoration(face)); - if(opts.fontSize!=null)parts.push('font-size:'+opts.fontSize+'em'); + if(opts.fontSize!=null)parts.push('font-size:'+opts.fontSize+(typeof opts.fontSize==='number'?'em':'')); const bx=boxCss(face.box,opts.boxBg); if(bx)parts.push('box-shadow:'+bx); return parts.join(';'); @@ -1162,6 +1162,15 @@ function parseHeightEntry(kind,raw){ } // The computed hint beside an absolute entry: 130 -> "= 13.0pt". function ptHint(height){return typeof height==='number'&&isFinite(height)?('= '+(height/10).toFixed(1)+'pt'):'';} +// CSS font-size for a face's height: a relative multiplier renders as em, an +// absolute 1/10pt value as true pt (the preview shows real size), unset or the +// identity 1 as null (inherit). The stored heightMode rules; a legacy object +// without one falls back to integer/fractional inference, same as the loader. +function heightCssValue(f){ + if(!f||typeof f.height!=='number'||!isFinite(f.height)||f.height===1)return null; + const kind=f.heightMode||(Number.isInteger(f.height)?'abs':'rel'); + return kind==='abs'?(f.height/10)+'pt':f.height+'em'; +} // Compose an element-hover tooltip: the face's docstring on top, the existing // hover text (e.g. the bare face name) below it, separated by a blank line. A @@ -2612,7 +2621,7 @@ function flashUiPreview(f){const sp=document.querySelectorAll(`#mockframe [data- function flashPkg(f){flashRow(document.querySelector(`#pkgbody tr[data-face="${f}"]`));} function flashPkgPreview(f){const sp=document.querySelectorAll(`#pkgpreview [data-face="${f}"]`);if(sp.length){flashEls(sp);return;}const row=document.querySelector(`#pkgbody tr[data-face="${f}"]`);if(row)flashEl(row.querySelector('.cat'));} function mockSpan(k,t){return `<span data-k="${k}" style="${syntaxStyle(k)}">${esc(t)}</span>`;} -function uiCss(o,fgv,bgv,opts={}){const fg=fgv===undefined?effFg(o.fg):fgv,bg=bgv===undefined?o.bg:bgv;return faceCss(o,fg,bg,{noBg:opts.noBg,boxBg:bg||MAP['bg']});} +function uiCss(o,fgv,bgv,opts={}){const fg=fgv===undefined?effFg(o.fg):fgv,bg=bgv===undefined?o.bg:bgv;return faceCss(o,fg,bg,{noBg:opts.noBg,fontSize:heightCssValue(o),boxBg:bg||MAP['bg']});} // Size a preview pane to its faces table, minus the label bar above it. Shared by // the UI mock and the package preview, which differ only in their element IDs. function syncPaneHeight(tableId,paneId){const t=document.getElementById(tableId),m=document.getElementById(paneId);if(!t||!m)return;const lb=m.previousElementSibling,lbh=lb?lb.getBoundingClientRect().height+10:30;m.style.height=Math.max(t.getBoundingClientRect().height-lbh,220)+'px';} @@ -2788,7 +2797,7 @@ function buildPkgTable(){ // app.js. Pure preview HTML builders (ofs/os/previewLines + renderXxxPreview); // they reference shared globals (PKGMAP, MAP, faceCss, effFg, ...) and are // inlined into the page's single script element via the PREVIEWS_J token in app.js. -function ofs(app,face){const f=PKGMAP[app][face]||{},fg=effFg(pkgEffFg(app,face)),bg=pkgEffBg(app,face);return faceCss(f,fg,bg,{fontSize:(f.height||1),boxBg:bg||MAP['bg']});} +function ofs(app,face){const f=PKGMAP[app][face]||{},fg=effFg(pkgEffFg(app,face)),bg=pkgEffBg(app,face);return faceCss(f,fg,bg,{fontSize:heightCssValue(f),boxBg:bg||MAP['bg']});} // The CSS for a UI-owned face rendered off any preview surface: effective fg // (floored to the default fg) and bg, following the built-in UI inherit chain so // the rendered color matches what the registry reports. The @ui counterpart to ofs. @@ -4067,7 +4076,7 @@ function worstCellHtml(face){ // Repaint every covered overlay face (their floors depend on the syntax palette, // so a syntax-color edit has to refresh them even though it doesn't rebuild the table). function repaintCovered(){COVERED_FACES.forEach(f=>{if(UIMAP[f]&&document.getElementById('uicr-'+f))paintUI(f);});} -function paintUI(face){const pv=document.getElementById('uiprev-'+face);if(!pv)return;const o=UIMAP[face];pv.style.color=effFg(o.fg);pv.style.background=effBg(o.bg);pv.style.fontWeight=cssWeight(o.weight);pv.style.fontStyle=o.slant||'normal';pv.style.textDecoration=(o.underline?'underline ':'')+(o.strike?'line-through':'')||'none';pv.style.boxShadow=boxCss(o.box,effBg(o.bg)); +function paintUI(face){const pv=document.getElementById('uiprev-'+face);if(!pv)return;const o=UIMAP[face];pv.style.color=effFg(o.fg);pv.style.background=effBg(o.bg);pv.style.fontWeight=cssWeight(o.weight);pv.style.fontStyle=o.slant||'normal';pv.style.fontSize=heightCssValue(o)||'';pv.style.textDecoration=(o.underline?'underline ':'')+(o.strike?'line-through':'')||'none';pv.style.boxShadow=boxCss(o.box,effBg(o.bg)); const report=coveredContrastReport(face); pv.title=''; const cr=document.getElementById('uicr-'+face);if(cr){cr.title='';const wc=worstCellHtml(face);if(wc!==null){cr.title=report.empty?'this overlay has no syntax foreground set yet':(failureTitle(report)||'all covered text clears '+WORST_TARGET.toFixed(1));cr.innerHTML=wc;}else{const efg=effFg(o.fg),ebg=effBg(o.bg),r=contrast(efg,ebg);cr.innerHTML=crHtml(r);}}} |
