aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/theme-studio')
-rw-r--r--scripts/theme-studio/app-core.js11
-rw-r--r--scripts/theme-studio/app.js21
-rw-r--r--scripts/theme-studio/face_coverage.py8
-rw-r--r--scripts/theme-studio/generate.py36
-rw-r--r--scripts/theme-studio/palette-generator-core.js3
-rw-r--r--scripts/theme-studio/test-locate.mjs18
-rw-r--r--scripts/theme-studio/test_generate.py33
-rw-r--r--scripts/theme-studio/theme-studio.html33
8 files changed, 81 insertions, 82 deletions
diff --git a/scripts/theme-studio/app-core.js b/scripts/theme-studio/app-core.js
index 966010f4c..0615faea7 100644
--- a/scripts/theme-studio/app-core.js
+++ b/scripts/theme-studio/app-core.js
@@ -708,13 +708,4 @@ function formatLocateTitle(meta){
return parts.concat(locateAttrsList(meta.attrs)).join(', ');
}
-// The immediate-wayfinding info line shown in the preview-label area on hover:
-// "section > face — value" (effective fg, plus bg when set). An unassigned meta
-// reads "<face> — unassigned". Terser than the title; the title is the full record.
-function locateInfoLine(meta){
- if(!meta||meta.unassigned)return (meta&&meta.face?meta.face:'')+' — unassigned';
- const val=meta.value.fg+(meta.value.bg?' / '+meta.value.bg:'');
- return meta.section+' > '+meta.face+' — '+val;
-}
-
-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, 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, previewFaceAttrs, isLocateOnPane, locateInfoLine };
+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, 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, previewFaceAttrs, isLocateOnPane };
diff --git a/scripts/theme-studio/app.js b/scripts/theme-studio/app.js
index b50315981..889d49528 100644
--- a/scripts/theme-studio/app.js
+++ b/scripts/theme-studio/app.js
@@ -81,15 +81,13 @@ function mkColorDropdown(options,cur,onPick,opts={}){
left.textContent='‹';right.textContent='›';left.title='move to next darker color in this column';right.title='move to next lighter color in this column';
const t=document.createElement('div');t.className='cdd'+(opts.compact?' compact':'');t.tabIndex=0;
const nameOf=h=>{const o=options.find(p=>p[0]===h);return o?o[1]:(h||'none');};
- const displayHex=h=>h||(opts.defaultHex||'');
- const displayName=h=>h?nameOf(h):(opts.defaultName||nameOf(h));
function step(dir){if(wrap.dataset.locked==='1')return;const next=spanNeighborHex(cur,PALETTE,groundPair(),dir);if(!next)return;cur=next;paint();onPick(next);}
function paintStepButtons(){
const locked=wrap.dataset.locked==='1';
left.disabled=locked||!spanNeighborHex(cur,PALETTE,groundPair(),-1);
right.disabled=locked||!spanNeighborHex(cur,PALETTE,groundPair(),1);
}
- function paint(){const shown=displayHex(cur),nm=displayName(cur),ttl=cur?(nm+' '+cur):(nm+(shown?' -> '+shown:''));t.style.background=shown||'#161412';t.style.color=shown?textOn(shown):'#b4b1a2';t.dataset.val=cur||'';t.title=ttl;t.classList.toggle('is-default',!cur);t.classList.toggle('gone',!!cur&&nameOf(cur)==='(gone)');
+ function paint(){const shown=cur||(opts.defaultHex||''),nm=cur?nameOf(cur):(opts.defaultName||nameOf(cur)),ttl=cur?(nm+' '+cur):(nm+(shown?' -> '+shown:''));t.style.background=shown||'#161412';t.style.color=shown?textOn(shown):'#b4b1a2';t.dataset.val=cur||'';t.title=ttl;t.classList.toggle('is-default',!cur);t.classList.toggle('gone',!!cur&&nameOf(cur)==='(gone)');
t.innerHTML=opts.compact?`<span class="cddsw" style="background:${shown||'transparent'}"></span>`:`<span class="cddsw" style="background:${shown||'transparent'}"></span>${esc(nm)}`;paintStepButtons();}
paint();
left.onclick=e=>{e.stopPropagation();step(-1);};
@@ -304,7 +302,7 @@ function clearUnlockedRows(items,keyFn,resetFn){
for(const it of items){const k=keyFn(it);if(k===null)continue;if(!LOCKED.has(k))resetFn(it);}
}
function rebuildColorTables(){
- buildTable();buildUITable();if(document.getElementById('pkgbody'))buildPkgTable();
+ buildTable();buildUITable();buildPkgTable();// buildPkgTable self-guards when #pkgbody is absent
}
function refreshPaletteState(opts={}){
renderPalette();rebuildColorTables();
@@ -595,7 +593,9 @@ 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 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';}
+// 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';}
function buildMockFrame(){
const fr=document.getElementById('mockframe');if(!fr)return;
rebuildLocateRegistry();
@@ -720,9 +720,9 @@ function onViewChange(){const s=document.getElementById('viewsel');const v=(s&&s
const show=(id,on)=>{const e=document.getElementById(id);if(e)e.style.display=on?'':'none';};
show('view-code',v==='@code');show('view-ui',v==='@ui');show('view-pkg',v[0]!=='@');
if(v==='@code')renderCode();
- else if(v==='@ui'){buildUITable();buildMockFrame();syncMockHeight();}
+ else if(v==='@ui'){buildUITable();buildMockFrame();syncPaneHeight('uitable','mockframe');}
else pkgChanged();}
-function pkgChanged(){buildPkgTable();buildPkgPreview();syncPkgHeight();}
+function pkgChanged(){buildPkgTable();buildPkgPreview();syncPaneHeight('pkgtable','pkgpreview');}
function buildPkgTable(){
const app=curApp(),tb=document.getElementById('pkgbody');if(!tb)return;tb.innerHTML='';
const flt=(document.getElementById('pkgfilter').value||'').trim().toLowerCase();
@@ -832,7 +832,6 @@ function buildPkgPreview(){
// no separate info line.
}
function resetApp(){const app=curApp();for(const [face,,d] of APPS[app].faces)if(!LOCKED.has('pkg:'+app+':'+face))PKGMAP[app][face]=seedFace(d);pkgChanged();notify('reset editable '+app+' faces to package defaults',false);}
-function syncPkgHeight(){const t=document.getElementById('pkgtable'),m=document.getElementById('pkgpreview');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';}
// --- worst-case readout for the covered overlay faces (spec Phase 4) ---------
// Default WCAG target for the worst-case verdict (AA). AAA is selectable.
let WORST_TARGET=4.5;
@@ -875,7 +874,7 @@ function repaintCovered(){COVERED_FACES.forEach(f=>{if(UIMAP[f]&&document.getEle
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));
const report=coveredContrastReport(face);
pv.title='';
- const cr=document.getElementById('uicr-'+face);if(cr){cr.title='';if(report!==null){if(report.empty){cr.title='this overlay has no syntax foreground set yet';cr.innerHTML='<span title="this overlay has no syntax foreground set yet">no fg set</span>';}else{const title=failureTitle(report)||'all covered text clears '+WORST_TARGET.toFixed(1);cr.title=title;cr.innerHTML=`<span style="color:${ratingColor(report.worst.ratio)}" title="${esc(title)}">${report.worst.ratio.toFixed(1)}</span>`;}}else{const efg=effFg(o.fg),ebg=effBg(o.bg),r=contrast(efg,ebg);cr.innerHTML=crHtml(r);}}}
+ 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);}}}
function buildUITable(){
const tb=document.getElementById('uibody');tb.innerHTML='';
for(const [face,label,ex] of UI_FACES){
@@ -918,9 +917,9 @@ function initApp(){
paletteShowFull=false; // open collapsed to base colors; the arrow expands the spans
buildLangSel();buildViewSel();renderPalette();rebuildColorTables();renderCode();applyGround();
initGeneratorControls();
- updateTitle();initPicker();buildPkgPreview();syncMockHeight();syncPkgHeight();
+ updateTitle();initPicker();buildPkgPreview();syncPaneHeight('uitable','mockframe');syncPaneHeight('pkgtable','pkgpreview');
onViewChange();
}
initApp();
-addEventListener('resize',()=>{syncMockHeight();syncPkgHeight();});
+addEventListener('resize',()=>{syncPaneHeight('uitable','mockframe');syncPaneHeight('pkgtable','pkgpreview');});
BROWSER_GATES_J
diff --git a/scripts/theme-studio/face_coverage.py b/scripts/theme-studio/face_coverage.py
index c6200e05c..57b44815a 100644
--- a/scripts/theme-studio/face_coverage.py
+++ b/scripts/theme-studio/face_coverage.py
@@ -179,12 +179,12 @@ def classify(name, items, src, pkgfaces):
if name == 'emacs-core':
return 'core'
c = collections.Counter(bucket_of_source(src.get(f, '')) for f in items)
- loaded = c['elpa'] + c['builtin'] + c['user'] + c['other']
- if loaded == 0:
+ elpa, builtin, user, other = c['elpa'], c['builtin'], c['user'], c['other']
+ if elpa + builtin + user + other == 0:
return 'package' if any(f in pkgfaces for f in items) else 'general'
- if c['elpa'] >= max(c['builtin'], c['user'], c['other']):
+ if elpa >= max(builtin, user, other):
return 'package'
- if c['other'] > c['builtin'] and c['other'] >= c['elpa']:
+ if other > builtin and other >= elpa:
return 'package'
return 'general'
diff --git a/scripts/theme-studio/generate.py b/scripts/theme-studio/generate.py
index 09c25d804..b673caefb 100644
--- a/scripts/theme-studio/generate.py
+++ b/scripts/theme-studio/generate.py
@@ -15,6 +15,22 @@ def read_json(name):
NERD_ICONS_LEGEND_FIELDS = ("key", "label", "face", "category", "glyph")
NERD_ICONS_GALLERY_GLYPH_FIELDS = ("glyph", "name")
+_NO_ARTIFACT = object() # distinguishes absent/malformed from a file that parsed to null
+
+def _load_nerd_icons_artifact(path, kind, tail):
+ """Open and JSON-parse the nerd-icons artifact at PATH. Return the parsed value,
+ or _NO_ARTIFACT (with a KIND/TAIL-labeled warning) when absent or malformed.
+ Shared skeleton for the legend and gallery loaders."""
+ if not os.path.exists(path):
+ print(f"WARNING: nerd-icons {kind} absent ({path}); {tail}")
+ return _NO_ARTIFACT
+ try:
+ with open(path) as src:
+ return json.load(src)
+ except (json.JSONDecodeError, OSError) as exc:
+ print(f"WARNING: nerd-icons {kind} malformed ({path}: {exc}); {tail}")
+ return _NO_ARTIFACT
+
def load_nerd_icons_legend(path=None):
"""Return the nerd-icons legend rows, or None when the artifact is unusable.
@@ -27,14 +43,8 @@ def load_nerd_icons_legend(path=None):
file, which lands here as None.
"""
path = path or os.path.join(HERE, "nerd-icons-legend.json")
- if not os.path.exists(path):
- print(f"WARNING: nerd-icons legend absent ({path}); generic nerd-icons app")
- return None
- try:
- with open(path) as src:
- data = json.load(src)
- except (json.JSONDecodeError, OSError) as exc:
- print(f"WARNING: nerd-icons legend malformed ({path}: {exc}); generic nerd-icons app")
+ data = _load_nerd_icons_artifact(path, "legend", "generic nerd-icons app")
+ if data is _NO_ARTIFACT:
return None
rows = data.get("legend") if isinstance(data, dict) else data
if not isinstance(rows, list) or not rows:
@@ -59,14 +69,8 @@ def load_nerd_icons_gallery(path=None):
the legend data still loads. Never raises.
"""
path = path or os.path.join(HERE, "nerd-icons-legend.json")
- if not os.path.exists(path):
- print(f"WARNING: nerd-icons gallery absent ({path}); legend without gallery")
- return None
- try:
- with open(path) as src:
- data = json.load(src)
- except (json.JSONDecodeError, OSError) as exc:
- print(f"WARNING: nerd-icons gallery malformed ({path}: {exc}); legend without gallery")
+ data = _load_nerd_icons_artifact(path, "gallery", "legend without gallery")
+ if data is _NO_ARTIFACT:
return None
groups = data.get("gallery") if isinstance(data, dict) else None
if not isinstance(groups, list) or not groups:
diff --git a/scripts/theme-studio/palette-generator-core.js b/scripts/theme-studio/palette-generator-core.js
index 6ad2bf44f..033fff373 100644
--- a/scripts/theme-studio/palette-generator-core.js
+++ b/scripts/theme-studio/palette-generator-core.js
@@ -50,8 +50,7 @@ function generatorHues(baseHue,scheme,count,rng){
const offsets=[0,120,240,30,150,270,60,180,300,90,210,330];
return offsets.slice(0,n).map(o=>(b+o)%360);
}
- if(scheme==='manual')return Array.from({length:n},(_,i)=>(b+(i*360)/n)%360);
- return Array.from({length:n},(_,i)=>(b+(i*360)/n)%360);
+ return Array.from({length:n},(_,i)=>(b+(i*360)/n)%360); // even spread (manual/default/unknown)
}
function generatorChroma(mode){
return mode==='subdued'?0.055:mode==='vivid'?0.13:0.085;
diff --git a/scripts/theme-studio/test-locate.mjs b/scripts/theme-studio/test-locate.mjs
index faac7f916..5e36aa808 100644
--- a/scripts/theme-studio/test-locate.mjs
+++ b/scripts/theme-studio/test-locate.mjs
@@ -8,7 +8,7 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
- buildLocateRegistry, locateFaceMeta, formatLocateTitle, previewFaceAttrs, isLocateOnPane, locateInfoLine,
+ buildLocateRegistry, locateFaceMeta, formatLocateTitle, previewFaceAttrs, isLocateOnPane,
} from './app-core.js';
// A constructed model: two package apps that BOTH own a face literally named
@@ -142,22 +142,6 @@ test('formatLocateTitle: Error — an unassigned meta reads "unassigned"', () =>
assert.equal(formatLocateTitle(locateFaceMeta('org-faces', 'ghost', reg)), 'ghost, unassigned');
});
-// --- locateInfoLine: "section > face — value" -------------------------------
-
-test('locateInfoLine: Normal — section > face — value (fg only, then fg / bg)', () => {
- const reg = buildLocateRegistry(APPS, PKGMAP, UIMAP, MAP);
- assert.equal(locateInfoLine(locateFaceMeta('org-faces', 'org-todo', reg)), 'org-faces > org-todo — #cc3333');
- const pkgmap = { app: { face: { fg: '#aabbcc', bg: '#223344', inherit: null, source: 'user' } } };
- const apps = { app: { label: 'App', faces: [['face', 'F', {}]] } };
- const reg2 = buildLocateRegistry(apps, pkgmap, {}, MAP);
- assert.equal(locateInfoLine(locateFaceMeta('app', 'face', reg2)), 'App > face — #aabbcc / #223344');
-});
-
-test('locateInfoLine: Error — an unassigned meta reads "<face> — unassigned"', () => {
- const reg = buildLocateRegistry(APPS, PKGMAP, UIMAP, MAP);
- assert.equal(locateInfoLine(locateFaceMeta('org-faces', 'ghost', reg)), 'ghost — unassigned');
-});
-
// --- previewFaceAttrs: owner-aware validation -------------------------------
test('previewFaceAttrs: Normal — a known owner/face validates; a bad owner is rejected', () => {
diff --git a/scripts/theme-studio/test_generate.py b/scripts/theme-studio/test_generate.py
index bc0e87815..3bc78bdf8 100644
--- a/scripts/theme-studio/test_generate.py
+++ b/scripts/theme-studio/test_generate.py
@@ -18,6 +18,39 @@ from collections import Counter, defaultdict
from contextlib import redirect_stdout
import generate # importable without side effects: the file write is __main__-guarded
+import face_coverage
+from unittest import mock
+
+
+class ClassifyBucket(unittest.TestCase):
+ """Characterization of face_coverage.classify's core/general/package decision,
+ locking each branch before the named-locals rewrite. bucket_of_source is mocked
+ to identity, so the src dict maps each face straight to its bucket name."""
+
+ def _classify(self, src, pkgfaces=(), name="x"):
+ with mock.patch.object(face_coverage, "bucket_of_source", lambda s: s):
+ return face_coverage.classify(name, list(src), src, set(pkgfaces))
+
+ def test_emacs_core_short_circuits_to_core(self):
+ self.assertEqual(face_coverage.classify("emacs-core", [], {}, set()), "core")
+
+ def test_nothing_loaded_with_a_package_face_is_package(self):
+ self.assertEqual(self._classify({"a": "unloaded", "b": "unloaded"}, pkgfaces={"b"}), "package")
+
+ def test_nothing_loaded_without_a_package_face_is_general(self):
+ self.assertEqual(self._classify({"a": "unloaded"}), "general")
+
+ def test_elpa_plurality_is_package(self):
+ self.assertEqual(self._classify({"a": "elpa", "b": "elpa", "c": "builtin"}), "package")
+
+ def test_elpa_tied_with_builtin_is_package(self):
+ self.assertEqual(self._classify({"a": "elpa", "b": "builtin"}), "package")
+
+ def test_other_beats_builtin_and_ties_elpa_is_package(self):
+ self.assertEqual(self._classify({"a": "other", "b": "other", "c": "elpa", "d": "builtin"}), "package")
+
+ def test_builtin_plurality_is_general(self):
+ self.assertEqual(self._classify({"a": "builtin", "b": "builtin", "c": "elpa"}), "general")
from app_inventory import face_rows
from default_faces import DefaultFaces, changed_summary
from face_specs import face_spec, package_face_spec, ui_face_spec
diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html
index b4e37b7a6..c35be0a48 100644
--- a/scripts/theme-studio/theme-studio.html
+++ b/scripts/theme-studio/theme-studio.html
@@ -1264,15 +1264,6 @@ function formatLocateTitle(meta){
}
return parts.concat(locateAttrsList(meta.attrs)).join(', ');
}
-
-// The immediate-wayfinding info line shown in the preview-label area on hover:
-// "section > face — value" (effective fg, plus bg when set). An unassigned meta
-// reads "<face> — unassigned". Terser than the title; the title is the full record.
-function locateInfoLine(meta){
- if(!meta||meta.unassigned)return (meta&&meta.face?meta.face:'')+' — unassigned';
- const val=meta.value.fg+(meta.value.bg?' / '+meta.value.bg:'');
- return meta.section+' > '+meta.face+' — '+val;
-}
// Pure color/UI-boundary helpers (normHex, ratingColor, textOn), inlined from
// app-util.js. textOn uses rl from the colormath core above.
// Pure color/UI-boundary helpers: hex-input parsing, the contrast-rating status
@@ -1352,8 +1343,7 @@ function generatorHues(baseHue,scheme,count,rng){
const offsets=[0,120,240,30,150,270,60,180,300,90,210,330];
return offsets.slice(0,n).map(o=>(b+o)%360);
}
- if(scheme==='manual')return Array.from({length:n},(_,i)=>(b+(i*360)/n)%360);
- return Array.from({length:n},(_,i)=>(b+(i*360)/n)%360);
+ return Array.from({length:n},(_,i)=>(b+(i*360)/n)%360); // even spread (manual/default/unknown)
}
function generatorChroma(mode){
return mode==='subdued'?0.055:mode==='vivid'?0.13:0.085;
@@ -1751,15 +1741,13 @@ function mkColorDropdown(options,cur,onPick,opts={}){
left.textContent='‹';right.textContent='›';left.title='move to next darker color in this column';right.title='move to next lighter color in this column';
const t=document.createElement('div');t.className='cdd'+(opts.compact?' compact':'');t.tabIndex=0;
const nameOf=h=>{const o=options.find(p=>p[0]===h);return o?o[1]:(h||'none');};
- const displayHex=h=>h||(opts.defaultHex||'');
- const displayName=h=>h?nameOf(h):(opts.defaultName||nameOf(h));
function step(dir){if(wrap.dataset.locked==='1')return;const next=spanNeighborHex(cur,PALETTE,groundPair(),dir);if(!next)return;cur=next;paint();onPick(next);}
function paintStepButtons(){
const locked=wrap.dataset.locked==='1';
left.disabled=locked||!spanNeighborHex(cur,PALETTE,groundPair(),-1);
right.disabled=locked||!spanNeighborHex(cur,PALETTE,groundPair(),1);
}
- function paint(){const shown=displayHex(cur),nm=displayName(cur),ttl=cur?(nm+' '+cur):(nm+(shown?' -> '+shown:''));t.style.background=shown||'#161412';t.style.color=shown?textOn(shown):'#b4b1a2';t.dataset.val=cur||'';t.title=ttl;t.classList.toggle('is-default',!cur);t.classList.toggle('gone',!!cur&&nameOf(cur)==='(gone)');
+ function paint(){const shown=cur||(opts.defaultHex||''),nm=cur?nameOf(cur):(opts.defaultName||nameOf(cur)),ttl=cur?(nm+' '+cur):(nm+(shown?' -> '+shown:''));t.style.background=shown||'#161412';t.style.color=shown?textOn(shown):'#b4b1a2';t.dataset.val=cur||'';t.title=ttl;t.classList.toggle('is-default',!cur);t.classList.toggle('gone',!!cur&&nameOf(cur)==='(gone)');
t.innerHTML=opts.compact?`<span class="cddsw" style="background:${shown||'transparent'}"></span>`:`<span class="cddsw" style="background:${shown||'transparent'}"></span>${esc(nm)}`;paintStepButtons();}
paint();
left.onclick=e=>{e.stopPropagation();step(-1);};
@@ -1974,7 +1962,7 @@ function clearUnlockedRows(items,keyFn,resetFn){
for(const it of items){const k=keyFn(it);if(k===null)continue;if(!LOCKED.has(k))resetFn(it);}
}
function rebuildColorTables(){
- buildTable();buildUITable();if(document.getElementById('pkgbody'))buildPkgTable();
+ buildTable();buildUITable();buildPkgTable();// buildPkgTable self-guards when #pkgbody is absent
}
function refreshPaletteState(opts={}){
renderPalette();rebuildColorTables();
@@ -2516,7 +2504,9 @@ 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 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';}
+// 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';}
function buildMockFrame(){
const fr=document.getElementById('mockframe');if(!fr)return;
rebuildLocateRegistry();
@@ -2641,9 +2631,9 @@ function onViewChange(){const s=document.getElementById('viewsel');const v=(s&&s
const show=(id,on)=>{const e=document.getElementById(id);if(e)e.style.display=on?'':'none';};
show('view-code',v==='@code');show('view-ui',v==='@ui');show('view-pkg',v[0]!=='@');
if(v==='@code')renderCode();
- else if(v==='@ui'){buildUITable();buildMockFrame();syncMockHeight();}
+ else if(v==='@ui'){buildUITable();buildMockFrame();syncPaneHeight('uitable','mockframe');}
else pkgChanged();}
-function pkgChanged(){buildPkgTable();buildPkgPreview();syncPkgHeight();}
+function pkgChanged(){buildPkgTable();buildPkgPreview();syncPaneHeight('pkgtable','pkgpreview');}
function buildPkgTable(){
const app=curApp(),tb=document.getElementById('pkgbody');if(!tb)return;tb.innerHTML='';
const flt=(document.getElementById('pkgfilter').value||'').trim().toLowerCase();
@@ -3281,7 +3271,6 @@ function buildPkgPreview(){
// no separate info line.
}
function resetApp(){const app=curApp();for(const [face,,d] of APPS[app].faces)if(!LOCKED.has('pkg:'+app+':'+face))PKGMAP[app][face]=seedFace(d);pkgChanged();notify('reset editable '+app+' faces to package defaults',false);}
-function syncPkgHeight(){const t=document.getElementById('pkgtable'),m=document.getElementById('pkgpreview');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';}
// --- worst-case readout for the covered overlay faces (spec Phase 4) ---------
// Default WCAG target for the worst-case verdict (AA). AAA is selectable.
let WORST_TARGET=4.5;
@@ -3324,7 +3313,7 @@ function repaintCovered(){COVERED_FACES.forEach(f=>{if(UIMAP[f]&&document.getEle
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));
const report=coveredContrastReport(face);
pv.title='';
- const cr=document.getElementById('uicr-'+face);if(cr){cr.title='';if(report!==null){if(report.empty){cr.title='this overlay has no syntax foreground set yet';cr.innerHTML='<span title="this overlay has no syntax foreground set yet">no fg set</span>';}else{const title=failureTitle(report)||'all covered text clears '+WORST_TARGET.toFixed(1);cr.title=title;cr.innerHTML=`<span style="color:${ratingColor(report.worst.ratio)}" title="${esc(title)}">${report.worst.ratio.toFixed(1)}</span>`;}}else{const efg=effFg(o.fg),ebg=effBg(o.bg),r=contrast(efg,ebg);cr.innerHTML=crHtml(r);}}}
+ 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);}}}
function buildUITable(){
const tb=document.getElementById('uibody');tb.innerHTML='';
for(const [face,label,ex] of UI_FACES){
@@ -3367,11 +3356,11 @@ function initApp(){
paletteShowFull=false; // open collapsed to base colors; the arrow expands the spans
buildLangSel();buildViewSel();renderPalette();rebuildColorTables();renderCode();applyGround();
initGeneratorControls();
- updateTitle();initPicker();buildPkgPreview();syncMockHeight();syncPkgHeight();
+ updateTitle();initPicker();buildPkgPreview();syncPaneHeight('uitable','mockframe');syncPaneHeight('pkgtable','pkgpreview');
onViewChange();
}
initApp();
-addEventListener('resize',()=>{syncMockHeight();syncPkgHeight();});
+addEventListener('resize',()=>{syncPaneHeight('uitable','mockframe');syncPaneHeight('pkgtable','pkgpreview');});
// Shared gate harness. Each call site keeps its literal location.hash==='#NAMEtest'
// check (run-tests.sh greps it); gate() owns the ok/notes/A setup and the verdict
// postamble. Note format standardized to ' fails=note1,note2'.