aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio/theme-studio.html
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/theme-studio/theme-studio.html')
-rw-r--r--scripts/theme-studio/theme-studio.html29
1 files changed, 24 insertions, 5 deletions
diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html
index 219a4a03..0e04d012 100644
--- a/scripts/theme-studio/theme-studio.html
+++ b/scripts/theme-studio/theme-studio.html
@@ -186,8 +186,7 @@ function seedPkgmap(){return buildPkgmap(APPS,PALETTE);}
let PKGMAP=seedPkgmap();
function esc(t){return t.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
// Pure color-math core (lin/rl/contrast/rating/hsv2rgb/rgb2hsv/hex2rgb/rgb2hex,
-// plus OKLab/OKLCH/APCA/deltaE), inlined verbatim from colormath.js. normHex,
-// textOn, and ratingColor stay below as UI-boundary helpers.
+// plus OKLab/OKLCH/APCA/deltaE), inlined verbatim from colormath.js.
// colormath.js — pure color-math core for theme-studio.
//
// One source of truth: node imports this module (tests); generate.py inlines its
@@ -407,8 +406,29 @@ function effResolve(map,app,face,attr,seen){seen=seen||{};const f=map[app]&&map[
// Standard swatch-dropdown option list: a default entry, then the palette. When
// cur is set but no longer in the palette, surface it as a "(gone)" entry first.
function optList(cur,palette){const have=cur===''||palette.some(p=>p[0]===cur);return [['','— default —'],...(have?palette:[[cur,'(gone) '+cur],...palette])];}
-function textOn(h){const L=rl(h);return ((L+0.05)/0.05)>(1.05/(L+0.05))?'#000':'#fff';}
+
+// Turn a theme name into a safe filename slug: collapse runs of disallowed
+// characters to a single dash, trim leading/trailing dashes, fall back to 'theme'.
+function slugify(name){return name.replace(/[^A-Za-z0-9._-]+/g,'-').replace(/^-+|-+$/g,'')||'theme';}
+// 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
+// color, and the readable text color for a background. These are kept out of
+// colormath.js (the pure math core) but are unit-tested and inlined into the page
+// the same way. textOn leans on rl from colormath; the import is for the tests —
+// generate.py strips it on inline, where rl is already present from the inlined
+// colormath core.
+
+// Normalize a hex string: trim, accept an optional leading #, require exactly six
+// hex digits, lowercase the result. Returns null for anything else.
+function normHex(s){s=s.trim();if(/^[0-9a-fA-F]{6}$/.test(s))s='#'+s;return /^#[0-9a-fA-F]{6}$/.test(s)?s.toLowerCase():null;}
+
+// Map a WCAG contrast ratio to a status color: AAA green (>=7), AA grey (>=4.5),
+// otherwise the fail red.
function ratingColor(r){return r>=7?'#5d9b86':r>=4.5?'#a9b2bb':'#cb6b4d';}
+
+// Pick black or white text for a background hex, by WCAG relative luminance.
+function textOn(h){const L=rl(h);return ((L+0.05)/0.05)>(1.05/(L+0.05))?'#000':'#fff';}
// The contrast-cell readout shared by every table: a WCAG ratio colored by its
// AA/AAA rating, with the rating word. Callers compute r for their own fg/bg.
function crHtml(r){return `<span style="color:${ratingColor(r)}">${r.toFixed(1)} ${rating(r)}</span>`;}
@@ -582,7 +602,6 @@ function updateColor(){
for(const ap in PKGMAP)for(const fc in PKGMAP[ap]){const o=PKGMAP[ap][fc];if(o.fg===oldHex)o.fg=newHex;if(o.bg===oldHex)o.bg=newHex;}
closePicker();renderPalette();buildTable();buildUITable();renderCode();applyGround();notify('updated "'+newName+'"',false);
}
-function normHex(s){s=s.trim();if(/^[0-9a-fA-F]{6}$/.test(s))s='#'+s;return /^#[0-9a-fA-F]{6}$/.test(s)?s.toLowerCase():null;}
function curHex(){return normHex(document.getElementById('newhexstr').value)||'#888888';}
let pkH=0,pkS=0,pkV=0.5,pickerOn=false;
let pkMode='any'; // contrast mask: any / aa / aaa (what constraint to mask)
@@ -659,7 +678,7 @@ function addColor(){const h=curHex();const name=document.getElementById('newname
if(PALETTE.some(p=>p[1].toLowerCase()===name.toLowerCase())){notify('a color named "'+name+'" already exists — select it and use Update selected to change its value',true);return;}
PALETTE.push([h,name]);document.getElementById('newname').value='';selectedIdx=null;closePicker();renderPalette();buildTable();notify('added "'+name+'"',false);}
function themeName(){return (document.getElementById('themename').value||'theme').trim()||'theme';}
-function fileSlug(){return themeName().replace(/[^A-Za-z0-9._-]+/g,'-').replace(/^-+|-+$/g,'')||'theme';}
+function fileSlug(){return slugify(themeName());}
function exportObj(){const a={};CATS.forEach(c=>a[c[0]]=MAP[c[0]]);const o={name:themeName(),palette:PALETTE,assignments:a,bold:Object.keys(BOLD).filter(k=>BOLD[k]),italic:Object.keys(ITALIC).filter(k=>ITALIC[k]),ui:UIMAP};if(LOCKED.size)o.locks=[...LOCKED];const pk=packagesForExport(PKGMAP);if(Object.keys(pk).length)o.packages=pk;return o;}
function exportState(){const t=document.getElementById('export');t.value=JSON.stringify(exportObj(),null,1);t.style.display='block';t.focus();t.select();}
function toggleJSON(){const t=document.getElementById('export'),b=document.getElementById('jsonbtn');if(t.style.display==='block'){t.style.display='none';b.textContent='show';}else{exportState();b.textContent='hide';}}