aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio/previews.js
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/theme-studio/previews.js')
-rw-r--r--scripts/theme-studio/previews.js154
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 &lt; 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,'&quot;');}
+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','&lt;')+os(a,'markdown-html-tag-name-face','kbd')+os(a,'markdown-html-tag-delimiter-face','&gt;')+'Ctrl-C'+os(a,'markdown-html-tag-delimiter-face','&lt;/')+os(a,'markdown-html-tag-name-face','kbd')+os(a,'markdown-html-tag-delimiter-face','&gt;'));
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>';}