diff options
Diffstat (limited to 'scripts/theme-studio/previews.js')
| -rw-r--r-- | scripts/theme-studio/previews.js | 154 |
1 files changed, 137 insertions, 17 deletions
diff --git a/scripts/theme-studio/previews.js b/scripts/theme-studio/previews.js index bef8b7c12..15e885da1 100644 --- a/scripts/theme-studio/previews.js +++ b/scripts/theme-studio/previews.js @@ -3,10 +3,47 @@ // 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 os(app,face,txt){return `<span data-face="${face}" style="${ofs(app,face)}">${txt}</span>`;} -// Shared wrapper for the line-based package previews: a monospace pre block. +// 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. +function ulocateCss(face){const o=UIMAP[face]||{},fg=effFg(resolveUiAttr(face,'fg',UIMAP)),bg=resolveUiAttr(face,'bg',UIMAP)||null;return faceCss(o,fg,bg,{boxBg:bg||MAP['bg']});} +// previewSpan -- the one stateful locate adapter (preview-locate spec). Reads the +// live globals (PKGMAP / UIMAP / MAP), dispatches by the owner's surface to the +// package (ofs / PKGMAP) or @ui (UIMAP) style path, and emits the shared locate +// attributes: data-owner-app (the internal owner key), data-face, and the +// locate-onpane class when the owner is the pane currently viewed. TEXT is trusted +// preview HTML -- callers pre-escape entities, matching the old os() contract, so +// previewSpan does not re-escape it (that would double-escape < etc.). os +// delegates here for package owners; an @ui or cross-package owner makes an +// off-pane, hover-only span. +function attresc(s){return esc(String(s)).replace(/"/g,'"');} +function previewSpan(owner,face,text){ + const style=owner==='@ui'?ulocateCss(face):ofs(owner,face); + const cls=isLocateOnPane(owner,curApp())?' class="locate-onpane"':''; + const title=attresc(formatLocateTitle(locateFaceMeta(owner,face,LOCATE_REG))); + return `<span data-owner-app="${owner}" data-face="${face}"${cls} title="${title}" style="${style}">${text}</span>`; +} +function os(app,face,txt){return previewSpan(app,face,txt);} +// 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 `<div style="padding:12px 16px;font:12pt/1.7 monospace;white-space:pre">${L.join('\n')}</div>`;} +function previewLines(L){return `<div style="padding:12px 16px;font:12pt/1.7 ${PREVIEW_FONT};white-space:pre">${L.join('\n')}</div>`;} 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')); @@ -133,21 +170,21 @@ function renderDashboardPreview(){const a='dashboard',L=[]; L.push(''); L.push(''); L.push(os(a,'dashboard-heading','Projects:')); - L.push(' ~/'); - L.push(' ~/.emacs.d/'); - L.push(' ~/projects/work/'); - L.push(' ~/org/roam/'); - L.push(' ~/projects/home/'); + L.push(os(a,'dashboard-items-face',' ~/')); + L.push(os(a,'dashboard-items-face',' ~/.emacs.d/')); + L.push(os(a,'dashboard-items-face',' ~/projects/work/')); + L.push(os(a,'dashboard-items-face',' ~/org/roam/')); + L.push(os(a,'dashboard-items-face',' ~/projects/home/')); L.push(''); L.push(os(a,'dashboard-heading','Bookmarks')); - L.push(' Cesar Aira, The Little Buddhist Monk & the Proof'); - L.push(' Edward Abbey, The Fool’s Progress: An Honest Novel'); - L.push(' Agatha Christie, The A.B.C. Murders'); + L.push(os(a,'dashboard-items-face',' Cesar Aira, The Little Buddhist Monk & the Proof')); + L.push(os(a,'dashboard-items-face',' Edward Abbey, The Fool’s Progress: An Honest Novel')); + L.push(os(a,'dashboard-items-face',' Agatha Christie, The A.B.C. Murders')); L.push(''); L.push(os(a,'dashboard-heading','Recent Files:')); - L.push(' theme-theme.el'); - L.push(' todo.org'); - L.push(' theme-studio-palette-generator-spec.org'); + L.push(os(a,'dashboard-items-face',' theme-theme.el')); + L.push(os(a,'dashboard-items-face',' todo.org')); + L.push(os(a,'dashboard-items-face',' theme-studio-palette-generator-spec.org')); return previewLines(L);} function renderMu4ePreview(){const a='mu4e',L=[]; const pad=(s,n)=>{s=String(s);return s.length>=n?s.slice(0,n):s+' '.repeat(n-s.length);}; @@ -256,6 +293,61 @@ function renderGitGutterPreview(){const a='git-gutter',L=[]; L.push(os(a,'git-gutter:deleted','_')+os(a,'git-gutter:separator','|')+' (deleted lines marker)'); L.push(os(a,'git-gutter:unchanged',' ')+os(a,'git-gutter:separator','|')+' '+os(a,'git-gutter:unchanged','unchanged line of code')); return previewLines(L);} +function renderEatPreview(){const a='eat',L=[],c=(f,t)=>os(a,'eat-term-color-'+f,t),x=(f,t)=>os(a,'eat-term-'+f,t),an=(g,t)=>os(a,'eat-shell-prompt-annotation-'+g,t); + const p=g=>an(g,g==='success'?'✔':g==='failure'?'✘':'…')+' ~/projects/app $ '; + // 1. directory listing -- the widest palette block (dircolors) + L.push(p('success')+'eza -la --color'); + L.push('drwxr-xr-x - 14:02 '+c('blue','.git/')); + L.push('.rw-r--r-- 120 09:11 .gitignore'); + L.push('drwxr-xr-x - 14:02 '+c('blue','src/')); + L.push('drwxr-xr-x - 13:48 '+c('blue','tests/')); + L.push('.rwxr-xr-x 2.1k 14:00 '+c('bright-green','run.sh')); + L.push('lrwxr-xr-x - 14:01 '+c('cyan','latest')+' -> '+c('blue','v2.1/')); + L.push('.rw-r--r-- 4.5M 22:30 '+c('red','backup.tar.gz')); + L.push('.rw-r--r-- 88k 18:05 '+c('magenta','logo.png')); + L.push('.rw-r--r-- 3.2k 14:02 README.md'); + L.push(''); + // 2. git status -- staged green, unstaged/untracked red + L.push(p('success')+'git status -sb'); + L.push(c('bright-cyan','## main...origin/main [ahead 2]')); + L.push(c('green','A src/eat-preview.js')); + L.push(c('green','A src/cache.el')); + L.push(c('green','M README.md')); + L.push(c('red',' M init.el')); + L.push(c('red',' M modules/term-config.el')); + L.push(c('red',' D modules/old-vterm.el')); + L.push(c('red','?? docs/design/eat.org')); + L.push(c('red','?? scratch.txt')); + L.push(''); + // 3. git log --decorate -- yellow hashes, colored refs, a merge + L.push(p('success')+'git log --oneline --graph --decorate'); + L.push(c('bright-black','* ')+c('yellow','a1b2c3d')+' '+c('bright-cyan','(HEAD -> ')+c('bright-green','main')+c('bright-cyan',')')+' richer eat preview blocks'); + L.push(c('bright-black','* ')+c('yellow','9f8e7d6')+' '+c('bright-yellow','(tag: v2.1, ')+c('bright-red','origin/main')+c('bright-yellow',')')+' lowercase the labels'); + L.push(c('bright-black','* ')+c('yellow','3c4d5e6')+' Merge branch '+c('green',"'eat-faces'")); + L.push(c('bright-black','|\\ ')); + L.push(c('bright-black','| * ')+c('yellow','7a8b9c0')+' expose eat faces to studio'); + L.push(c('bright-black','| * ')+c('yellow','1d2e3f4')+' add eat-term-color docstrings'); + L.push(c('bright-black','|/ ')); + L.push(c('bright-black','* ')+c('yellow','5f6a7b8')+' toggle eat instead of ghostel on f12'); + L.push(c('bright-black','* ')+c('yellow','2c3d4e5')+' calendar-sync robustness fixes'); + L.push(''); + // 4. test run -- pass green, skip yellow, fail red, bold summary, faint detail + L.push(p('failure')+'make test'); + L.push(c('green','✔ PASS')+' term-toggle '+x('faint','(19 tests)')); + L.push(c('green','✔ PASS')+' ai-term '+x('faint','(158 tests)')); + L.push(c('green','✔ PASS')+' calendar-sync '+x('faint','(575 tests)')); + L.push(c('green','✔ PASS')+' dashboard '+x('faint','(18 tests)')); + L.push(c('yellow','⚠ SKIP')+' network-sync '+x('faint','(2 tests, offline)')); + L.push(c('green','✔ PASS')+' transcription '+x('faint','(44 tests)')); + L.push(c('red','✘ FAIL')+' org-roam-refile '+x('faint','(1 test)')); + L.push(' '+x('italic','expected 3 refile targets, got 0')); + L.push(x('bold','Ran 817 tests, 815 passed, ')+c('yellow','1 skipped, ')+c('red','1 failed')+' '+x('faint','0.84s')); + L.push(''); + // swatch reference key, below the realistic blocks + L.push(x('faint','palette')+' '+c('black','■')+c('red','■')+c('green','■')+c('yellow','■')+c('blue','■')+c('magenta','■')+c('cyan','■')+c('white','■')+' '+c('bright-black','■')+c('bright-red','■')+c('bright-green','■')+c('bright-yellow','■')+c('bright-blue','■')+c('bright-magenta','■')+c('bright-cyan','■')+c('bright-white','■')); + L.push(x('faint','attrs')+' '+x('bold','bold')+' '+x('faint','faint')+' '+x('italic','italic')+' '+x('slow-blink','slow-blink')+' '+x('fast-blink','fast-blink')); + L.push(x('faint','prompt')+' '+an('success','✔ ok')+' '+an('running','… running')+' '+an('failure','✘ failed')); + return previewLines(L);} function renderFlycheckPreview(){const a='flycheck',L=[]; L.push(os(a,'flycheck-fringe-error','E')+os(a,'flycheck-fringe-warning','W')+os(a,'flycheck-fringe-info','I')+' x = '+os(a,'flycheck-error','undefined_name')+'('+os(a,'flycheck-warning','unused_arg')+') '+os(a,'flycheck-info','# note')); L.push(' '+os(a,'flycheck-error-delimiter','[')+os(a,'flycheck-delimited-error','err')+os(a,'flycheck-error-delimiter',']')); @@ -382,7 +474,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='<div style="padding:10px 14px;font:12pt/1.8 monospace">';for(const [face,label] of APPS[app].faces)h+=`<div data-face="${face}" style="${ofs(app,face)}">${esc(label)}</div>`;return h+'</div>';} +function genericPreview(app){let h='<div style="padding:10px 14px;font:12pt/1.8 '+PREVIEW_FONT+'">';for(const [face,label] of APPS[app].faces)h+=`<div data-face="${face}" style="${ofs(app,face)}">${esc(label)}</div>`;return h+'</div>';} // 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 @@ -412,8 +504,8 @@ function renderAutodimPreview(){ const accent=uf('cursor').bg||'#67809c'; const pane=(label,body,bg,focused)=> `<div style="flex:1;min-width:20ch;border:${focused?'2px solid '+accent:'1px solid #2a2a2a'};border-radius:4px;overflow:hidden">` - +`<div style="text-align:center;font:bold 10pt monospace;padding:4px;color:${focused?'#cdced1':'#8a8a8a'};background:${focused?'#1a1a1a':'#0a0a0a'};border-bottom:1px solid #2a2a2a">${label}</div>` - +`<div style="padding:10px 12px;font:12pt/1.6 monospace;white-space:pre;background:${bg}">${body}</div></div>`; + +`<div style="text-align:center;font:bold 10pt ${PREVIEW_FONT};padding:4px;color:${focused?'#cdced1':'#8a8a8a'};background:${focused?'#1a1a1a':'#0a0a0a'};border-bottom:1px solid #2a2a2a">${label}</div>` + +`<div style="padding:10px 12px;font:12pt/1.6 ${PREVIEW_FONT};white-space:pre;background:${bg}">${body}</div></div>`; const litBody=lit+'\n'+`<span style="color:#5e6770">${esc(foldText)}</span>`; const dimBody=`<span data-face="auto-dim-other-buffers" style="color:${dimFg}">${dim}</span>\n` +`<span data-face="auto-dim-other-buffers-hide" style="color:${hideFg};background:${hideBg}">${esc(foldText)}</span>`; @@ -461,3 +553,31 @@ function renderMarkdownPreview(){const a='markdown-mode',L=[]; L.push(os(a,'markdown-html-tag-delimiter-face','<')+os(a,'markdown-html-tag-name-face','kbd')+os(a,'markdown-html-tag-delimiter-face','>')+'Ctrl-C'+os(a,'markdown-html-tag-delimiter-face','</')+os(a,'markdown-html-tag-name-face','kbd')+os(a,'markdown-html-tag-delimiter-face','>')); L.push(os(a,'markdown-footnote-marker-face','[^1]:')+' '+os(a,'markdown-footnote-text-face','the footnote text.')); return previewLines(L);} +// nerd-icons gallery grid: the full colored catalog. Every distinct face-bearing +// nerd-icons glyph (APPS['nerd-icons'].gallery, captured by build-nerd-icons-legend.el), +// one row per color face, the rows ordered by hue so families cluster (blues +// together, reds together). Each cell draws the glyph in its face color with the +// icon's nerd-font name beneath. SIZEPT (points, default 14) sizes the glyphs so +// the designer can view the grid at different buffer sizes via the preview-pane +// dropdown; the cell width scales with it. Recoloring a face repaints its swatch +// and every glyph in its row because os() reads the live registry. Falls back to +// the generic preview if the gallery is missing (the bespoke app registers with a +// valid legend, so that path is defensive). +function renderNerdIconsPreview(sizePt){ + const a='nerd-icons',groups=(APPS[a]&&APPS[a].gallery)||[]; + if(!groups.length)return genericPreview(a); + const pt=sizePt||14,cellW=Math.round(pt*4.6+24); + let h=`<div class="ni-gallery" style="padding:10px 14px;font:10pt/1.4 ${PREVIEW_FONT}">`; + for(const g of groups){ + h+='<div class="ni-row" style="margin:0 0 10px;border-top:1px solid #2a2a2a;padding-top:6px">' + +`<div class="ni-row-head" style="color:#8a8a8a;padding:0 0 5px">` + +os(a,g.face,'■')+' '+esc(g.face)+' ('+g.glyphs.length+')</div>' + +'<div class="ni-cells">'; + for(const e of g.glyphs) + h+=`<span class="ni-cell" style="display:inline-block;width:${cellW}px;text-align:center;vertical-align:top;margin:3px 1px">` + +`<span style="font-size:${pt}pt;line-height:1.3">`+os(a,g.face,e.glyph)+'</span><br>' + +`<span style="font-size:7.5pt;color:#9a9a9a;word-break:break-all;line-height:1.2">`+esc(e.name)+'</span>' + +'</span>'; + h+='</div></div>'; + } + return h+'</div>';} |
