From c5ca8b7d7ac1aa751c1bf79ad35b178f96b3ba77 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Tue, 23 Jun 2026 19:34:01 -0400 Subject: feat(theme-studio): locate preview elements by hover and click Hovering a data-face preview element shows its section, face, and effective value in the preview-label info line, and the element's title carries the full record: effective fg/bg plus a per-attribute source note (direct, inherited-from-X, default, or cleared-rendering-as-default). Clicking an on-pane element scrolls to and flashes its assignment row. Off-pane and cross-surface elements stay hover-only. A single owner-qualified registry keyed by {owner, face} backs both data-face surfaces, package and UI, so the same face name under two owners never collides. The pure helpers in app-core.js take all state as arguments and return data. The one stateful adapter, previewSpan, lives in previews.js and emits the escaped markup. os() stays a package-owner wrapper over previewSpan, and a unified locateClick dispatcher replaces the per-surface click branches. Covered by test-locate.mjs and four new browser gates. Full harness green. --- scripts/theme-studio/app.js | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) (limited to 'scripts/theme-studio/app.js') diff --git a/scripts/theme-studio/app.js b/scripts/theme-studio/app.js index 85570e213..28b8e3cdb 100644 --- a/scripts/theme-studio/app.js +++ b/scripts/theme-studio/app.js @@ -20,6 +20,16 @@ const DEFAULT_SYNTAX=JSON.parse(JSON.stringify(SYNTAX)); function pname(n){return nameToHex(n,PALETTE);} function seedPkgmap(){return buildPkgmap(APPS,PALETTE);} let PKGMAP=seedPkgmap(); +// Preview-locate registry (preview-locate spec). One cached, module-level +// registry rebuilt once per assignment / import / reset / view-switch batch — at +// the top of the two preview renderers (buildPkgPreview, buildMockFrame), which +// every such path funnels through before spans render. Never rebuilt per hover or +// per span. locate-onpane is recomputed from the current view at render time +// (isLocateOnPane), never stored here. Built lazily (not at declaration): the +// inlined buildLocateRegistry / UI_INHERIT from app-core.js are spliced below +// this point, so an init call here would hit the const's temporal dead zone. +let LOCATE_REG={}; +function rebuildLocateRegistry(){LOCATE_REG=buildLocateRegistry(APPS,PKGMAP,UIMAP,MAP);return LOCATE_REG;} function esc(t){return t.replace(/&/g,'&').replace(//g,'>');} // Pure color-math core (lin/rl/contrast/rating/hsv2rgb/rgb2hsv/hex2rgb/rgb2hex, // plus OKLab/OKLCH/APCA/deltaE), inlined verbatim from colormath.js. @@ -558,6 +568,21 @@ function mkBoxControl(get,set,opts={}){ get,set, Object.assign({styled:true,toState:(v,cur)=>({style:v,width:(cur&&cur.width)||1,color:(cur&&cur.color)||null})},opts));} function flashRow(tr){if(!tr)return;tr.scrollIntoView({block:'center',behavior:'smooth'});tr.classList.remove('flash');void tr.offsetWidth;tr.classList.add('flash');} +// Unified preview-locate click dispatch (preview-locate spec, Phases 4-5). One +// handler for every preview surface replaces the per-surface data-face branches: +// find the clicked data-face element, resolve its owner (data-owner-app, or +// DEFAULTOWNER for a bare span emitted by the generic / auto-dim / UI-mock +// renderers that pre-date previewSpan), and flash its assignment row only when it +// is on-pane. An owner-tagged off-pane / unassigned element is inert; a bare span +// is a current-pane element by construction, so it stays clickable. No persistent +// selection — flashRow is scroll + flash only. The data-k syntax-click path stays +// separate (handled by each caller before delegating here). +function locateClick(e,defaultOwner){ + const u=e.target.closest('[data-face]');if(!u)return; + if(u.dataset.ownerApp&&!u.classList.contains('locate-onpane'))return; + const owner=u.dataset.ownerApp||defaultOwner; + if(owner==='@ui')flashUi(u.dataset.face);else flashPkg(u.dataset.face); +} function flashEl(el){if(!el)return;el.scrollIntoView({block:'nearest',inline:'nearest',behavior:'smooth'});el.classList.remove('flashtok');void el.offsetWidth;el.classList.add('flashtok');} // Flash every matching element but scroll only the first into view, so a face // that maps to several preview spans still lands the viewport on the first. @@ -573,6 +598,7 @@ function uiCss(o,fgv,bgv,opts={}){const fg=fgv===undefined?effFg(o.fg):fgv,bg=bg function syncMockHeight(){const t=document.getElementById('uitable'),m=document.getElementById('mockframe');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';} function buildMockFrame(){ const fr=document.getElementById('mockframe');if(!fr)return; + rebuildLocateRegistry(); const bg=MAP['bg'],fg=MAP['p']; const ln=uf('line-number'),lnc=uf('line-number-current-line'),hl=uf('hl-line'),hil=uf('highlight'),reg=uf('region'),isr=uf('isearch'),isf=uf('isearch-fail'),laz=uf('lazy-highlight'),par=uf('show-paren-match'),parx=uf('show-paren-mismatch'),cur=uf('cursor'),ml=uf('mode-line'),mli=uf('mode-line-inactive'),mlh=uf('mode-line-highlight'),mb=uf('minibuffer-prompt'),frng=uf('fringe'),vb=uf('vertical-border'),lnk=uf('link'),err=uf('error'),wrn=uf('warning'),suc=uf('success'); const lines=[ @@ -641,7 +667,7 @@ function buildMockFrame(){ html+=`
I-search: count zzz [no match]
`; html+=`
https://gnu.org error warning ok
`; fr.innerHTML=html;fr.style.background=bg;fr.style.color=fg; - fr.onclick=(e)=>{const u=e.target.closest('[data-face]');if(u){flashUi(u.dataset.face);return;}const k=e.target.closest('[data-k]');if(k)flashAssign(k.dataset.k);}; + fr.onclick=(e)=>{if(e.target.closest('[data-face]')){locateClick(e,'@ui');return;}const k=e.target.closest('[data-k]');if(k)flashAssign(k.dataset.k);}; } // All three tiers share one dropdown — the swatch div from mkColorDropdown. The // native