diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-24 18:10:38 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-24 18:10:38 -0400 |
| commit | fd1969eda49b2e6c562439ecea9430ace164e16d (patch) | |
| tree | c4a9f3874987300845ec180758191d6e59beb163 /scripts/theme-studio | |
| parent | 201a1174ce1e6004087fc53271deac7eac22555a (diff) | |
| download | dotemacs-fd1969eda49b2e6c562439ecea9430ace164e16d.tar.gz dotemacs-fd1969eda49b2e6c562439ecea9430ace164e16d.zip | |
refactor(theme-studio): tier-1 simplification pass
These are behavior-preserving cleanups from the refactor/simplify assessment, all test-verified.
I merged syncMockHeight and syncPkgHeight into one syncPaneHeight(tableId, paneId), inlined the two single-use displayHex/displayName closures, dropped a pkgbody guard that buildPkgTable already does, and had paintUI call worstCellHtml instead of rebuilding the covered-contrast cell. I deleted the dead generatorHues "manual" branch (a copy of the fallback) and locateInfoLine (orphaned when I removed the preview info line earlier today). The two nerd-icons loaders now share _load_nerd_icons_artifact, with a sentinel so a null-file edge keeps its exact behavior. face_coverage.classify reads through named locals now, guarded by a new characterization test.
Two assessment findings were wrong and skipped after I checked them against the code: LOCATE_REG is live (read by previewSpan), and normalizePaletteEntryCore doesn't exist.
Diffstat (limited to 'scripts/theme-studio')
| -rw-r--r-- | scripts/theme-studio/app-core.js | 11 | ||||
| -rw-r--r-- | scripts/theme-studio/app.js | 21 | ||||
| -rw-r--r-- | scripts/theme-studio/face_coverage.py | 8 | ||||
| -rw-r--r-- | scripts/theme-studio/generate.py | 36 | ||||
| -rw-r--r-- | scripts/theme-studio/palette-generator-core.js | 3 | ||||
| -rw-r--r-- | scripts/theme-studio/test-locate.mjs | 18 | ||||
| -rw-r--r-- | scripts/theme-studio/test_generate.py | 33 | ||||
| -rw-r--r-- | scripts/theme-studio/theme-studio.html | 33 |
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'. |
