From c11ad211f5d72b6ee2b48d80f25d16e3e85248eb Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Wed, 24 Jun 2026 14:04:48 -0400 Subject: fix(theme-studio): render nerd-icon glyphs in previews instead of tofu The legend, dashboard, and package previews drew nerd-icon glyphs as empty boxes. The font-family never reached them: PREVIEW_FONT was spliced into inline style="..." attributes with a double-quoted family name, so the inner quote closed the attribute early and the font was silently dropped. Dropping the quotes fixes it. A no-space family name needs none. I embedded the glyph font directly: Symbols Nerd Font Mono, encoded with fontTools (woff2_compress output is rejected by headed Chrome and Firefox), inlined as a data: URI under the unique family name ThemeStudioNerd so it resolves to the embed rather than a system-installed copy of the same name. The page is self-contained and renders on any clone. I added a #fonttest gate that parses previewLines output and asserts the resolved font-family plus glyph coverage, plus a make font target that re-encodes the woff2 with fontTools. --- scripts/theme-studio/previews.js | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) (limited to 'scripts/theme-studio/previews.js') diff --git a/scripts/theme-studio/previews.js b/scripts/theme-studio/previews.js index fef616c40..592640cea 100644 --- a/scripts/theme-studio/previews.js +++ b/scripts/theme-studio/previews.js @@ -24,9 +24,26 @@ function previewSpan(owner,face,text){ return `${text}`; } function os(app,face,txt){return previewSpan(app,face,txt);} -// Shared wrapper for the line-based package previews: a monospace pre block. +// Preview font stack: the embedded @font-face (family "ThemeStudioNerd", +// Symbols Nerd Font Mono inlined as a data: URI in styles.css) supplies the nerd +// glyphs; monospace supplies everything else. The family name is deliberately +// custom, NOT the real "Symbols Nerd Font Mono": when the @font-face name matches +// a font the user has installed system-wide, Chrome resolves the family to the +// local copy instead of our embedded one and the glyphs render as tofu (the +// embedded font only wins in environments without that system font, e.g. headless +// CI). A unique family name forces the embedded font. "ThemeStudioNerd" carries +// only icon glyphs, so plain text falls through to monospace and the layout is +// unchanged — only the nerd codepoints pull from the embedded font. +// NOTE: the family name is UNQUOTED here on purpose. PREVIEW_FONT is interpolated +// into inline style="..." attributes (previewLines, genericPreview, the mock +// frame), and a double-quoted family name inside a double-quoted attribute +// terminates the attribute early, silently dropping the font-family (the glyphs +// then fall back to monospace = tofu). A no-space identifier needs no quotes, so +// keep ThemeStudioNerd quote-free and never reintroduce a spaced/quoted name here. +const PREVIEW_FONT='ThemeStudioNerd,monospace'; +// Shared wrapper for the line-based package previews: a nerd-font pre block. // Each renderer builds its own L array of os(...) lines and returns previewLines(L). -function previewLines(L){return `
${L.join('\n')}
`;} +function previewLines(L){return `
${L.join('\n')}
`;} function renderOrgPreview(){const a='org-mode',L=[]; L.push(os(a,'org-document-info-keyword','#+TITLE:')+' '+os(a,'org-document-title','Project Notes')); L.push(os(a,'org-document-info-keyword','#+AUTHOR:')+' '+os(a,'org-document-info','Craig Jennings')); @@ -402,7 +419,7 @@ function renderTelegaPreview(){const a='telega',L=[]; L.push(os(a,'telega-link-preview-sitename','example.com')+' '+os(a,'telega-link-preview-title','Link preview title')); L.push('Webpage '+os(a,'telega-webpage-title','Title')+' '+os(a,'telega-webpage-subtitle','Subtitle')+' '+os(a,'telega-webpage-header','Header')+' '+os(a,'telega-webpage-subheader','Subheader')+' '+os(a,'telega-webpage-outline','outline')+' '+os(a,'telega-webpage-fixed','fixed')+' '+os(a,'telega-webpage-preformatted','pre')+' '+os(a,'telega-webpage-marked','marked')+' '+os(a,'telega-webpage-strike-through','strike')+' '+os(a,'telega-webpage-chat-link','chat-link')); return previewLines(L);} -function genericPreview(app){let h='
';for(const [face,label] of APPS[app].faces)h+=`
${esc(label)}
`;return h+'
';} +function genericPreview(app){let h='
';for(const [face,label] of APPS[app].faces)h+=`
${esc(label)}
`;return h+'
';} // Bespoke split preview: a focused window beside its auto-dimmed twin, both // showing the language selected at the top of the page (kept in sync via the // langsel onchange, which re-runs buildPkgPreview). The left pane carries the @@ -432,8 +449,8 @@ function renderAutodimPreview(){ const accent=uf('cursor').bg||'#67809c'; const pane=(label,body,bg,focused)=> `
` - +`
${label}
` - +`
${body}
`; + +`
${label}
` + +`
${body}
`; const litBody=lit+'\n'+`${esc(foldText)}`; const dimBody=`${dim}\n` +`${esc(foldText)}`; -- cgit v1.2.3