bluedither 1.0.21 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -161,7 +161,24 @@ function autoWireVitePlugin(targetDir) {
161
161
  const p = resolve(targetDir, v);
162
162
  if (existsSync(p)) { configPath = p; break; }
163
163
  }
164
- if (!configPath) return;
164
+
165
+ // No vite config exists — check if vite is available and create one
166
+ if (!configPath) {
167
+ const pkgPath = resolve(targetDir, 'package.json');
168
+ const hasVite = existsSync(pkgPath) &&
169
+ JSON.parse(readFileSync(pkgPath, 'utf-8')).devDependencies?.vite;
170
+
171
+ if (hasVite) {
172
+ // Vite is installed but no config — create a minimal one with the plugin
173
+ configPath = resolve(targetDir, 'vite.config.js');
174
+ writeFileSync(configPath, `import { bluedither } from './bluedither/dev-middleware.js'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n plugins: [bluedither()],\n})\n`);
175
+ console.log(`Created vite.config.js with BlueDither plugin`);
176
+ return;
177
+ }
178
+
179
+ // No vite at all — nothing to wire
180
+ return;
181
+ }
165
182
 
166
183
  const config = readFileSync(configPath, 'utf-8');
167
184
 
@@ -1,8 +1,8 @@
1
- (()=>{if(!window.__BD_TUNER_LOADED__){let w=function(n,r,e){let i=r.split("."),t=n;for(let d=0;d<i.length-1;d++)t=t[i[d]];t[i[i.length-1]]=e},L=function(n,r){return r.split(".").reduce((e,i)=>e?.[i],n)},k=function(n,r){document.documentElement.style.setProperty(n,r)},v=function(n,r){let e=window.__BD_SHADER__;e&&e.updateParams({[n]:r})},U=function(n){let r=b.layout.designWidth,e=n/16,i=n/r*100;return`clamp(${(e*.55).toFixed(4)}rem, ${i.toFixed(4)}vw, ${e.toFixed(4)}rem)`},$=function(n){let r="bd-gf-"+n.replace(/\s+/g,"-").toLowerCase();if(document.getElementById(r))return;let e=document.createElement("link");e.id=r,e.rel="stylesheet",e.href=`https://fonts.googleapis.com/css2?family=${encodeURIComponent(n)}:wght@400;700&display=swap`,document.head.appendChild(e)},_=function(n){let r=document.createElement("div");return r.className="bd-tuner-section",r.innerHTML=`<div class="bd-tuner-section-label">${n}</div>`,f.appendChild(r),r},C=function(n,r,e,i,t){let d=L(b,e)||"#000000",a=document.createElement("div");a.className="bd-tuner-row",a.innerHTML=`
1
+ (()=>{if(!window.__BD_TUNER_LOADED__){let w=function(n,r,e){let i=r.split("."),t=n;for(let d=0;d<i.length-1;d++)t=t[i[d]];t[i[i.length-1]]=e},L=function(n,r){return r.split(".").reduce((e,i)=>e?.[i],n)},v=function(n,r){document.documentElement.style.setProperty(n,r)},S=function(n,r){let e=window.__BD_SHADER__;e&&e.updateParams({[n]:r})},U=function(n){let r=b.layout.designWidth,e=n/16,i=n/r*100;return`clamp(${(e*.55).toFixed(4)}rem, ${i.toFixed(4)}vw, ${e.toFixed(4)}rem)`},$=function(n){let r="bd-gf-"+n.replace(/\s+/g,"-").toLowerCase();if(document.getElementById(r))return;let e=document.createElement("link");e.id=r,e.rel="stylesheet",e.href=`https://fonts.googleapis.com/css2?family=${encodeURIComponent(n)}:wght@400;700&display=swap`,document.head.appendChild(e)},_=function(n){let r=document.createElement("div");return r.className="bd-tuner-section",r.innerHTML=`<div class="bd-tuner-section-label">${n}</div>`,f.appendChild(r),r},C=function(n,r,e,i,t){let d=L(b,e)||"#000000",a=document.createElement("div");a.className="bd-tuner-row",a.innerHTML=`
2
2
  <span class="bd-tuner-label">${r}</span>
3
3
  <span class="bd-tuner-input"><input type="color" value="${d.substring(0,7)}"></span>
4
4
  <span class="bd-tuner-value">${d.substring(0,7)}</span>
5
- `;let s=a.querySelector("input"),c=a.querySelector(".bd-tuner-value");return s.addEventListener("input",l=>{let g=l.target.value;w(b,e,g),c.textContent=g,i&&k(i,g),t&&t(g)}),n.appendChild(a),{setValue(l){w(b,e,l),s.value=l.substring(0,7),c.textContent=l.substring(0,7),i&&k(i,l)}}},m=function(n,r,e,i,t,d,a){let s=L(b,e),c=document.createElement("div");c.className="bd-tuner-row bd-tuner-row-stacked",c.innerHTML=`
5
+ `;let s=a.querySelector("input"),c=a.querySelector(".bd-tuner-value");return s.addEventListener("input",l=>{let m=l.target.value;w(b,e,m),c.textContent=m,i&&v(i,m),t&&t(m)}),n.appendChild(a),{setValue(l){w(b,e,l),s.value=l.substring(0,7),c.textContent=l.substring(0,7),i&&v(i,l)}}},g=function(n,r,e,i,t,d,a){let s=L(b,e),c=document.createElement("div");c.className="bd-tuner-row bd-tuner-row-stacked",c.innerHTML=`
6
6
  <div class="bd-tuner-row-top">
7
7
  <span class="bd-tuner-label">${r}</span>
8
8
  <div class="bd-tuner-px-input-wrap">
@@ -11,24 +11,24 @@
11
11
  </div>
12
12
  </div>
13
13
  <input type="range" class="bd-tuner-slider" min="${i}" max="${t}" step="${d}" value="${s}">
14
- `;let l=c.querySelector(".bd-tuner-px-num"),g=c.querySelector(".bd-tuner-slider");function u(o){if(o=Math.max(i,Math.min(t,o)),w(b,e,o),l.value=o,g.value=o,a){let S=U(o);(Array.isArray(a)?a:[a]).forEach(h=>k(h,S))}}l.addEventListener("input",o=>u(parseFloat(o.target.value)||0)),g.addEventListener("input",o=>u(parseFloat(o.target.value))),l.addEventListener("keydown",o=>{if(o.key==="ArrowUp"||o.key==="ArrowDown"){o.preventDefault();let S=o.shiftKey?10:1,h=(o.key==="ArrowUp"?d:-d)*S;u(parseFloat(l.value)+h)}}),n.appendChild(c)},P=function(n,r,e,i,t,d,a){let s=L(b,e),c=document.createElement("div");c.className="bd-tuner-row bd-tuner-row-stacked",c.innerHTML=`
14
+ `;let l=c.querySelector(".bd-tuner-px-num"),m=c.querySelector(".bd-tuner-slider");function u(o){if(o=Math.max(i,Math.min(t,o)),w(b,e,o),l.value=o,m.value=o,a){let k=U(o);(Array.isArray(a)?a:[a]).forEach(h=>v(h,k))}}l.addEventListener("input",o=>u(parseFloat(o.target.value)||0)),m.addEventListener("input",o=>u(parseFloat(o.target.value))),l.addEventListener("keydown",o=>{if(o.key==="ArrowUp"||o.key==="ArrowDown"){o.preventDefault();let k=o.shiftKey?10:1,h=(o.key==="ArrowUp"?d:-d)*k;u(parseFloat(l.value)+h)}}),n.appendChild(c)},E=function(n,r,e,i,t,d,a){let s=L(b,e),c=document.createElement("div");c.className="bd-tuner-row bd-tuner-row-stacked",c.innerHTML=`
15
15
  <div class="bd-tuner-row-top">
16
16
  <span class="bd-tuner-label">${r}</span>
17
17
  <span class="bd-tuner-value">${s}</span>
18
18
  </div>
19
19
  <input type="range" class="bd-tuner-slider" min="${i}" max="${t}" step="${d}" value="${s}">
20
- `;let l=c.querySelector(".bd-tuner-slider"),g=c.querySelector(".bd-tuner-value");l.addEventListener("input",u=>{let o=parseFloat(u.target.value);w(b,e,o),g.textContent=o,a&&a(o)}),n.appendChild(c)},D=function(n,r,e,i){let t=L(b,e),d=document.createElement("div");d.className="bd-tuner-row",d.innerHTML=`
20
+ `;let l=c.querySelector(".bd-tuner-slider"),m=c.querySelector(".bd-tuner-value");l.addEventListener("input",u=>{let o=parseFloat(u.target.value);w(b,e,o),m.textContent=o,a&&a(o)}),n.appendChild(c)},D=function(n,r,e,i){let t=L(b,e),d=document.createElement("div");d.className="bd-tuner-row",d.innerHTML=`
21
21
  <span class="bd-tuner-label">${r}</span>
22
22
  <span class="bd-tuner-input bd-tuner-font-input">
23
23
  <input type="text" class="bd-tuner-font-search" value="${t}" placeholder="Search fonts...">
24
24
  <div class="bd-tuner-font-dropdown"></div>
25
25
  </span>
26
- `;let a=d.querySelector(".bd-tuner-font-search"),s=d.querySelector(".bd-tuner-font-dropdown"),c=g=>z.some(u=>u.toLowerCase()===g.toLowerCase());function l(g=""){let u=q.filter(o=>o.toLowerCase().includes(g.toLowerCase())).slice(0,20);s.innerHTML=u.map(o=>{let S=c(o)?' <span style="opacity:0.4;font-size:9px">SYSTEM</span>':"";return`<div class="bd-tuner-font-option" data-font="${o}" style="font-family:'${o}',system-ui">${o}${S}</div>`}).join(""),u.filter(o=>!c(o)).forEach($),s.querySelectorAll(".bd-tuner-font-option").forEach(o=>{o.addEventListener("mousedown",S=>{S.preventDefault();let h=o.dataset.font;a.value=h,w(b,e,h),c(h)||$(h),k(i,`"${h}", system-ui, sans-serif`),s.classList.remove("open")})})}a.addEventListener("focus",()=>{l(a.value),s.classList.add("open")}),a.addEventListener("input",()=>{l(a.value),s.classList.add("open")}),a.addEventListener("blur",()=>{setTimeout(()=>s.classList.remove("open"),150)}),a.addEventListener("keydown",g=>{if(g.key==="Enter"){let u=a.value.trim();u&&(w(b,e,u),c(u)||$(u),k(i,`"${u}", system-ui, sans-serif`),s.classList.remove("open"),a.blur())}}),n.appendChild(d)},B=function(n,r,e,i,t){let d=L(b,e),a=document.createElement("div");a.className="bd-tuner-row bd-tuner-row-stacked",a.innerHTML=`
26
+ `;let a=d.querySelector(".bd-tuner-font-search"),s=d.querySelector(".bd-tuner-font-dropdown"),c=m=>z.some(u=>u.toLowerCase()===m.toLowerCase());function l(m=""){let u=q.filter(o=>o.toLowerCase().includes(m.toLowerCase())).slice(0,20);s.innerHTML=u.map(o=>{let k=c(o)?' <span style="opacity:0.4;font-size:9px">SYSTEM</span>':"";return`<div class="bd-tuner-font-option" data-font="${o}" style="font-family:'${o}',system-ui">${o}${k}</div>`}).join(""),u.filter(o=>!c(o)).forEach($),s.querySelectorAll(".bd-tuner-font-option").forEach(o=>{o.addEventListener("mousedown",k=>{k.preventDefault();let h=o.dataset.font;a.value=h,w(b,e,h),c(h)||$(h),v(i,`"${h}", system-ui, sans-serif`),s.classList.remove("open")})})}a.addEventListener("focus",()=>{l(a.value),s.classList.add("open")}),a.addEventListener("input",()=>{l(a.value),s.classList.add("open")}),a.addEventListener("blur",()=>{setTimeout(()=>s.classList.remove("open"),150)}),a.addEventListener("keydown",m=>{if(m.key==="Enter"){let u=a.value.trim();u&&(w(b,e,u),c(u)||$(u),v(i,`"${u}", system-ui, sans-serif`),s.classList.remove("open"),a.blur())}}),n.appendChild(d)},B=function(n,r,e,i,t){let d=L(b,e),a=document.createElement("div");a.className="bd-tuner-row bd-tuner-row-stacked",a.innerHTML=`
27
27
  <span class="bd-tuner-label">${r}</span>
28
28
  <div class="bd-tuner-segmented">
29
29
  ${i.map(s=>`<button class="bd-tuner-seg-btn${s===d?" active":""}" data-val="${s}">${s}</button>`).join("")}
30
30
  </div>
31
- `,a.querySelectorAll(".bd-tuner-seg-btn").forEach(s=>{s.addEventListener("click",()=>{a.querySelectorAll(".bd-tuner-seg-btn").forEach(l=>l.classList.remove("active")),s.classList.add("active");let c=s.dataset.val;w(b,e,c),t&&t(c)})}),n.appendChild(a)};window.__BD_TUNER_LOADED__=!0;let b=structuredClone(window.__BD_TOKENS__||JSON.parse(document.querySelector('script[type="application/json"][data-bluedither-tokens]')?.textContent||"{}")),O=structuredClone(window.__BD_DEFAULTS__||JSON.parse(document.querySelector('script[type="application/json"][data-bluedither-defaults]')?.textContent||"{}")),H=!!(window.__BD_TOKENS__&&window.__BD_DEFAULTS__)||!!window.__BD_TUNER_SERVER_MODE__,z=["Arial","Arial Black","Bebas Neue Pro","Consolas","Courier New","Georgia","Helvetica","Impact","Lucida Console","Segoe UI","Tahoma","Times New Roman","Trebuchet MS","Verdana","SF Pro Display","SF Mono","Cascadia Code","Menlo","Monaco"],j=["Bebas Neue","Space Mono","Inter","Roboto","Roboto Mono","Roboto Condensed","Open Sans","Montserrat","Lato","Oswald","Raleway","Poppins","Nunito","Playfair Display","Merriweather","Source Sans 3","Source Code Pro","PT Sans","PT Serif","PT Mono","Ubuntu","Ubuntu Mono","Fira Sans","Fira Code","Fira Mono","Work Sans","Noto Sans","Noto Serif","DM Sans","DM Serif Display","DM Mono","IBM Plex Sans","IBM Plex Mono","IBM Plex Serif","JetBrains Mono","Inconsolata","Space Grotesk","Archivo","Archivo Black","Barlow","Barlow Condensed","Lexend","Outfit","Sora","Manrope","Bitter","Crimson Text","Libre Baskerville","Abril Fatface","Anton","Permanent Marker","Righteous","Orbitron","Teko","Rubik","Quicksand","Cabin","Karla","Josefin Sans","Comfortaa","Fredoka","Geologica","Instrument Sans","Instrument Serif"],q=[...new Set([...z,...j])].sort(),f=document.createElement("div");f.id="bd-tuner";let F=document.createElement("button");F.id="bd-tuner-toggle",F.textContent="Tuner",F.onclick=()=>f.classList.remove("collapsed"),document.body.appendChild(f),document.body.appendChild(F),f.innerHTML=`
31
+ `,a.querySelectorAll(".bd-tuner-seg-btn").forEach(s=>{s.addEventListener("click",()=>{a.querySelectorAll(".bd-tuner-seg-btn").forEach(l=>l.classList.remove("active")),s.classList.add("active");let c=s.dataset.val;w(b,e,c),t&&t(c)})}),n.appendChild(a)};window.__BD_TUNER_LOADED__=!0;let b=structuredClone(window.__BD_TOKENS__||JSON.parse(document.querySelector('script[type="application/json"][data-bluedither-tokens]')?.textContent||"{}")),O=structuredClone(window.__BD_DEFAULTS__||JSON.parse(document.querySelector('script[type="application/json"][data-bluedither-defaults]')?.textContent||"{}")),H=!!(window.__BD_TOKENS__&&window.__BD_DEFAULTS__)||!!window.__BD_TUNER_SERVER_MODE__,z=["Arial","Arial Black","Bebas Neue Pro","Consolas","Courier New","Georgia","Helvetica","Impact","Lucida Console","Segoe UI","Tahoma","Times New Roman","Trebuchet MS","Verdana","SF Pro Display","SF Mono","Cascadia Code","Menlo","Monaco"],j=["Bebas Neue","Space Mono","Inter","Roboto","Roboto Mono","Roboto Condensed","Open Sans","Montserrat","Lato","Oswald","Raleway","Poppins","Nunito","Playfair Display","Merriweather","Source Sans 3","Source Code Pro","PT Sans","PT Serif","PT Mono","Ubuntu","Ubuntu Mono","Fira Sans","Fira Code","Fira Mono","Work Sans","Noto Sans","Noto Serif","DM Sans","DM Serif Display","DM Mono","IBM Plex Sans","IBM Plex Mono","IBM Plex Serif","JetBrains Mono","Inconsolata","Space Grotesk","Archivo","Archivo Black","Barlow","Barlow Condensed","Lexend","Outfit","Sora","Manrope","Bitter","Crimson Text","Libre Baskerville","Abril Fatface","Anton","Permanent Marker","Righteous","Orbitron","Teko","Rubik","Quicksand","Cabin","Karla","Josefin Sans","Comfortaa","Fredoka","Geologica","Instrument Sans","Instrument Serif"],q=[...new Set([...z,...j])].sort(),f=document.createElement("div");f.id="bd-tuner";let P=document.createElement("button");P.id="bd-tuner-toggle",P.textContent="Tuner",P.onclick=()=>f.classList.remove("collapsed"),document.body.appendChild(f),document.body.appendChild(P),f.innerHTML=`
32
32
  <div class="bd-tuner-title">
33
33
  <span>BlueDither Tuner</span>
34
34
  <div style="display:flex;gap:8px;align-items:center;">
@@ -36,7 +36,7 @@
36
36
  <button id="bd-tuner-close" title="Close">&times;</button>
37
37
  </div>
38
38
  </div>
39
- `,f.querySelector("#bd-tuner-close").onclick=()=>f.classList.add("collapsed"),f.querySelector("#bd-tuner-reset").onclick=async()=>{confirm("Reset all tokens to defaults?")&&(Object.assign(b,structuredClone(O)),await R(),location.reload())};let T=_("Colors");C(T,"Background","colors.background","--bd-bg");let N;C(T,"Primary","colors.primary","--bd-primary",n=>{N.setValue(n),v("colorFront",n)}),C(T,"Text","colors.text","--bd-text"),C(T,"CTA Background","colors.ctaBackground","--bd-cta-bg"),C(T,"CTA Text","colors.ctaText","--bd-cta-text"),N=C(T,"Shader Front","colors.shaderFront",null,n=>v("colorFront",n));let y=_("Typography");D(y,"Primary Font","typography.primaryFont","--bd-font-primary"),D(y,"Secondary Font","typography.secondaryFont","--bd-font-secondary"),m(y,"Headline Size","typography.headline.referencePx",32,300,1,"--bd-headline-size"),m(y,"Headline LH","typography.headline.lineHeightPx",24,280,1,"--bd-headline-lh"),m(y,"Sub Size","typography.subHeadline.referencePx",10,48,1,"--bd-sub-size"),m(y,"Sub LH","typography.subHeadline.lineHeightPx",12,80,1,"--bd-sub-lh"),m(y,"Logo Size","typography.logo.referencePx",12,80,1,"--bd-logo-size"),m(y,"Nav Size","typography.navItem.referencePx",10,48,1,"--bd-nav-size");let x=_("Spacing");m(x,"Header Pad X","spacing.headerPaddingX",0,80,1,"--bd-header-px"),m(x,"Header Pad Y","spacing.headerPaddingY",0,60,1,"--bd-header-py"),m(x,"Hero Pad Top","spacing.heroPaddingTop",0,120,1,"--bd-hero-pt"),m(x,"Hero Pad Bottom","spacing.heroPaddingBottom",0,120,1,"--bd-hero-pb"),m(x,"Hero Pad X","spacing.heroPaddingX",0,120,1,"--bd-hero-px"),m(x,"Nav Gap","spacing.navGap",0,100,1,"--bd-nav-gap"),m(x,"CTA Pad X","spacing.ctaPaddingX",0,60,1,"--bd-cta-px"),m(x,"CTA Pad Y","spacing.ctaPaddingY",0,30,1,"--bd-cta-py"),m(x,"CTA Radius","spacing.ctaBorderRadius",0,32,1,"--bd-cta-radius");let E=_("Shader");B(E,"Shape","shader.shape",["warp","simplex","dots","wave","ripple","swirl","sphere"],n=>v("shape",n)),B(E,"Dither Type","shader.type",["random","2x2","4x4","8x8"],n=>v("type",n)),P(E,"Speed","shader.speed",0,2,.01,n=>v("speed",n)),P(E,"Scale","shader.scale",.1,5,.01,n=>v("scale",n)),P(E,"Dither Size","shader.size",.5,10,.1,n=>v("size",n));let I=_("Opacity");P(I,"Nav Links","opacity.navLinks",0,1,.01,n=>k("--bd-nav-opacity",n));let p=document.createElement("button");p.id="bd-tuner-commit",p.textContent="Commit Changes";let M=null;async function R(){let n=JSON.stringify(b,null,2);try{let t=await fetch("/__bluedither/commit",{method:"POST",headers:{"Content-Type":"application/json"},body:n});if(t.ok&&(await t.json()).ok)return"saved"}catch{}try{let t=await fetch("http://localhost:3344/commit",{method:"POST",headers:{"Content-Type":"application/json"},body:n});if(t.ok&&(await t.json()).ok)return"saved"}catch{}if(H)try{let t=await fetch("/commit",{method:"POST",headers:{"Content-Type":"application/json"},body:n});if(t.ok&&(await t.json()).ok)return"saved"}catch{}if(window.showSaveFilePicker)try{M||(M=await window.showSaveFilePicker({suggestedName:"tokens.json",types:[{description:"JSON",accept:{"application/json":[".json"]}}]}));let t=await M.createWritable();return await t.write(n),await t.close(),"saved"}catch(t){if(t.name==="AbortError")return"cancelled";M=null}let r=new Blob([n],{type:"application/json"}),e=URL.createObjectURL(r),i=document.createElement("a");return i.href=e,i.download="tokens.json",i.click(),URL.revokeObjectURL(e),"downloaded"}p.onclick=async()=>{p.textContent="Saving...",p.disabled=!0;try{let n=await R();if(n==="cancelled"){p.textContent="Commit Changes",p.disabled=!1;return}p.textContent=n==="saved"?"Saved!":"Downloaded!",p.classList.add("success"),setTimeout(()=>{p.textContent="Commit Changes",p.classList.remove("success"),p.disabled=!1},2e3)}catch(n){p.textContent="Error: "+n.message,p.disabled=!1,setTimeout(()=>{p.textContent="Commit Changes"},3e3)}},f.appendChild(p)}var A=document.createElement("style");A.textContent=`/* =============================================
39
+ `,f.querySelector("#bd-tuner-close").onclick=()=>f.classList.add("collapsed"),f.querySelector("#bd-tuner-reset").onclick=async()=>{confirm("Reset all tokens to defaults?")&&(Object.assign(b,structuredClone(O)),await R(),location.reload())};let T=_("Colors");C(T,"Background","colors.background","--bd-bg");let N;C(T,"Primary","colors.primary","--bd-primary",n=>{N.setValue(n),S("colorFront",n)}),C(T,"Text","colors.text","--bd-text"),C(T,"CTA Background","colors.ctaBackground","--bd-cta-bg"),C(T,"CTA Text","colors.ctaText","--bd-cta-text"),N=C(T,"Shader Front","colors.shaderFront",null,n=>S("colorFront",n));let y=_("Typography");D(y,"Primary Font","typography.primaryFont","--bd-font-primary"),D(y,"Secondary Font","typography.secondaryFont","--bd-font-secondary"),g(y,"Headline Size","typography.headline.referencePx",32,300,1,"--bd-headline-size"),g(y,"Headline LH","typography.headline.lineHeightPx",24,280,1,"--bd-headline-lh"),g(y,"Sub Size","typography.subHeadline.referencePx",10,48,1,"--bd-sub-size"),g(y,"Sub LH","typography.subHeadline.lineHeightPx",12,80,1,"--bd-sub-lh"),g(y,"Logo Size","typography.logo.referencePx",12,80,1,"--bd-logo-size"),g(y,"Nav Size","typography.navItem.referencePx",10,48,1,"--bd-nav-size");let x=_("Spacing");g(x,"Header Pad X","spacing.headerPaddingX",0,80,1,"--bd-header-px"),g(x,"Header Pad Y","spacing.headerPaddingY",0,60,1,"--bd-header-py"),g(x,"Hero Pad Top","spacing.heroPaddingTop",0,120,1,"--bd-hero-pt"),g(x,"Hero Pad Bottom","spacing.heroPaddingBottom",0,120,1,"--bd-hero-pb"),g(x,"Hero Pad X","spacing.heroPaddingX",0,120,1,"--bd-hero-px"),g(x,"Nav Gap","spacing.navGap",0,100,1,"--bd-nav-gap"),g(x,"CTA Pad X","spacing.ctaPaddingX",0,60,1,"--bd-cta-px"),g(x,"CTA Pad Y","spacing.ctaPaddingY",0,30,1,"--bd-cta-py"),E(x,"CTA Radius","spacing.ctaBorderRadius",0,32,1,n=>v("--bd-cta-radius",`${(n/16).toFixed(4)}rem`));let F=_("Shader");B(F,"Shape","shader.shape",["warp","simplex","dots","wave","ripple","swirl","sphere"],n=>S("shape",n)),B(F,"Dither Type","shader.type",["random","2x2","4x4","8x8"],n=>S("type",n)),E(F,"Speed","shader.speed",0,2,.01,n=>S("speed",n)),E(F,"Scale","shader.scale",.1,5,.01,n=>S("scale",n)),E(F,"Dither Size","shader.size",.5,10,.1,n=>S("size",n));let I=_("Opacity");E(I,"Nav Links","opacity.navLinks",0,1,.01,n=>v("--bd-nav-opacity",n));let p=document.createElement("button");p.id="bd-tuner-commit",p.textContent="Commit Changes";let M=null;async function R(){let n=JSON.stringify(b,null,2);try{let t=await fetch("/__bluedither/commit",{method:"POST",headers:{"Content-Type":"application/json"},body:n});if(t.ok&&(await t.json()).ok)return"saved"}catch{}try{let t=await fetch("http://localhost:3344/commit",{method:"POST",headers:{"Content-Type":"application/json"},body:n});if(t.ok&&(await t.json()).ok)return"saved"}catch{}if(H)try{let t=await fetch("/commit",{method:"POST",headers:{"Content-Type":"application/json"},body:n});if(t.ok&&(await t.json()).ok)return"saved"}catch{}if(window.showSaveFilePicker)try{M||(M=await window.showSaveFilePicker({suggestedName:"tokens.json",types:[{description:"JSON",accept:{"application/json":[".json"]}}]}));let t=await M.createWritable();return await t.write(n),await t.close(),"saved"}catch(t){if(t.name==="AbortError")return"cancelled";M=null}let r=new Blob([n],{type:"application/json"}),e=URL.createObjectURL(r),i=document.createElement("a");return i.href=e,i.download="tokens.json",i.click(),URL.revokeObjectURL(e),"downloaded"}p.onclick=async()=>{p.textContent="Saving...",p.disabled=!0;try{let n=await R();if(n==="cancelled"){p.textContent="Commit Changes",p.disabled=!1;return}p.textContent=n==="saved"?"Saved!":"Downloaded!",p.classList.add("success"),setTimeout(()=>{p.textContent="Commit Changes",p.classList.remove("success"),p.disabled=!1},2e3)}catch(n){p.textContent="Error: "+n.message,p.disabled=!1,setTimeout(()=>{p.textContent="Commit Changes"},3e3)}},f.appendChild(p)}var A=document.createElement("style");A.textContent=`/* =============================================
40
40
  BlueDither Fine-Tuner \u2014 Overlay Panel v2
41
41
  ============================================= */
42
42
 
@@ -59,8 +59,8 @@
59
59
 
60
60
  // Typography
61
61
  if (t.typography) {
62
- if (t.typography.primaryFont) root.style.setProperty('--bd-font-primary', t.typography.primaryFont);
63
- if (t.typography.secondaryFont) root.style.setProperty('--bd-font-secondary', t.typography.secondaryFont);
62
+ if (t.typography.primaryFont) root.style.setProperty('--bd-font-primary', `"${t.typography.primaryFont}", system-ui, sans-serif`);
63
+ if (t.typography.secondaryFont) root.style.setProperty('--bd-font-secondary', `"${t.typography.secondaryFont}", system-ui, sans-serif`);
64
64
  if (t.typography.headline) {
65
65
  root.style.setProperty('--bd-headline-size', pxToClamp(t.typography.headline.referencePx));
66
66
  root.style.setProperty('--bd-headline-lh', pxToClamp(t.typography.headline.lineHeightPx));
@@ -379,7 +379,7 @@ addPxField(spacingSection, 'Hero Pad X', 'spacing.heroPaddingX', 0, 120, 1, '--b
379
379
  addPxField(spacingSection, 'Nav Gap', 'spacing.navGap', 0, 100, 1, '--bd-nav-gap');
380
380
  addPxField(spacingSection, 'CTA Pad X', 'spacing.ctaPaddingX', 0, 60, 1, '--bd-cta-px');
381
381
  addPxField(spacingSection, 'CTA Pad Y', 'spacing.ctaPaddingY', 0, 30, 1, '--bd-cta-py');
382
- addPxField(spacingSection, 'CTA Radius', 'spacing.ctaBorderRadius', 0, 32, 1, '--bd-cta-radius');
382
+ addRange(spacingSection, 'CTA Radius', 'spacing.ctaBorderRadius', 0, 32, 1, (v) => setCSSVar('--bd-cta-radius', `${(v / 16).toFixed(4)}rem`));
383
383
 
384
384
  // ── Shader ──
385
385
  const shaderSection = addSection('Shader');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bluedither",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "A bold, dithered-shader hero theme for Claude Code — skill + fine-tuner",
5
5
  "type": "module",
6
6
  "bin": {
@@ -130,7 +130,14 @@ Note: The shader uses `ShaderMount` directly from `paper-shaders-bundle.js` —
130
130
  The `window.__BD_SHADER__` assignment is **required** — the tuner calls `updateParams()` on it for live shader editing. The ShaderMount instance has `setUniforms()` and `setSpeed()` methods. The tuner's `updateParams` maps token names to uniform names (e.g., `colorFront` → `u_colorFront`).
131
131
 
132
132
  ### `src/bluedither.css`
133
- All CSS custom properties and `.bd-*` class rules.
133
+ All CSS custom properties and `.bd-*` class rules. The CSS variable names MUST match exactly:
134
+
135
+ **Colors:** `--bd-bg`, `--bd-primary`, `--bd-text`, `--bd-cta-bg`, `--bd-cta-text`
136
+ **Typography:** `--bd-font-primary`, `--bd-font-secondary`, `--bd-font-primary-weight`, `--bd-font-secondary-weight`, `--bd-headline-size`, `--bd-headline-lh`, `--bd-sub-size`, `--bd-sub-lh`, `--bd-sub-transform`, `--bd-logo-size`, `--bd-logo-lh`, `--bd-nav-size`, `--bd-nav-lh`, `--bd-cta-size`, `--bd-cta-lh`
137
+ **Spacing:** `--bd-header-px`, `--bd-header-py`, `--bd-hero-pt`, `--bd-hero-pb`, `--bd-hero-px`, `--bd-nav-gap`, `--bd-cta-px`, `--bd-cta-py`, `--bd-cta-radius`
138
+ **Opacity:** `--bd-nav-opacity`
139
+
140
+ Using different variable names will break live updates from the tuner.
134
141
 
135
142
  ## Vite Config
136
143
 
@@ -28,20 +28,105 @@ Include `<link>` tags for `typography.primaryFont` and `typography.secondaryFont
28
28
  ```
29
29
  URL-encode the font family name.
30
30
 
31
+ ## CRITICAL: File Placement Rules
32
+
33
+ **In project root (or `src/`):**
34
+ - `index.html` — the rendered page
35
+ - `paper-shaders-bundle.js` — copied from `bluedither/shaders/`
36
+
37
+ **In `public/bluedither/` (for Vite) or alongside `index.html` (for non-Vite):**
38
+ - `bluedither-tuner.js` — tuner panel
39
+ - `bluedither-tuner.css` — tuner styles
40
+ - `bluedither-tuner-inject.js` — tuner loader
41
+ - `tokens.json` — design tokens (read at runtime by tuner)
42
+ - `tokens.defaults.json` — default tokens
43
+ - `dev-middleware.js` — Vite commit endpoint
44
+
45
+ Copy tuner assets:
46
+ ```bash
47
+ mkdir -p public/bluedither
48
+ cp bluedither/bluedither-tuner.js public/bluedither/
49
+ cp bluedither/bluedither-tuner.css public/bluedither/
50
+ cp bluedither/bluedither-tuner-inject.js public/bluedither/
51
+ cp bluedither/tokens.json public/bluedither/
52
+ cp bluedither/tokens.defaults.json public/bluedither/
53
+ cp bluedither/dev-middleware.js public/bluedither/
54
+ ```
55
+
31
56
  ## Shader Module
32
57
 
33
- Copy `theme/shaders/bluedither-shader.js` and `theme/shaders/paper-shaders-bundle.js` to the target project.
58
+ Copy `bluedither/shaders/paper-shaders-bundle.js` to the project root (or `src/`).
59
+
60
+ Import and initialize in a `<script type="module">` at the end of `<body>`. **CRITICAL:** You MUST create a `window.__BD_SHADER__` wrapper with an `updateParams()` method — the tuner calls `updateParams()` but ShaderMount only has `setUniforms()` and `setSpeed()`.
34
61
 
35
- Import and initialize inline. **CRITICAL**: Assign to `window.__BD_SHADER__` so the tuner can update shader params live:
36
62
  ```js
37
- import { ShaderMount, ditheringFragmentShader, getShaderColorFromString } from './paper-shaders-bundle.js';
63
+ import { ShaderMount, ditheringFragmentShader, DitheringShapes, DitheringTypes, getShaderColorFromString } from './paper-shaders-bundle.js';
64
+
65
+ const parent = document.getElementById('bd-shader-parent');
66
+ const mount = new ShaderMount(parent, ditheringFragmentShader, {
67
+ u_colorFront: getShaderColorFromString('#005A6A'),
68
+ u_colorBack: getShaderColorFromString('#00000000'),
69
+ u_shape: DitheringShapes.ripple,
70
+ u_type: DitheringTypes['2x2'],
71
+ u_pxSize: 4.2,
72
+ u_scale: 1,
73
+ u_rotation: 180,
74
+ u_originX: 0.5, u_originY: 0.5,
75
+ u_worldWidth: 0, u_worldHeight: 0,
76
+ u_fit: 0, u_offsetX: 0, u_offsetY: 0,
77
+ }, { alpha: true, premultipliedAlpha: false }, 0.41, 0, 2);
78
+
79
+ // CRITICAL: wrapper with updateParams for the tuner
80
+ window.__BD_SHADER__ = {
81
+ updateParams(params) {
82
+ const uniforms = {};
83
+ if (params.colorFront !== undefined) uniforms.u_colorFront = getShaderColorFromString(params.colorFront);
84
+ if (params.colorBack !== undefined) uniforms.u_colorBack = getShaderColorFromString(params.colorBack);
85
+ if (params.shape !== undefined) uniforms.u_shape = DitheringShapes[params.shape] ?? DitheringShapes.warp;
86
+ if (params.type !== undefined) uniforms.u_type = DitheringTypes[params.type] ?? DitheringTypes['2x2'];
87
+ if (params.size !== undefined) uniforms.u_pxSize = params.size;
88
+ if (params.scale !== undefined) uniforms.u_scale = params.scale;
89
+ if (params.rotation !== undefined) uniforms.u_rotation = parseFloat(params.rotation) || 180;
90
+ if (Object.keys(uniforms).length > 0) mount.setUniforms(uniforms);
91
+ if (params.speed !== undefined) mount.setSpeed(params.speed);
92
+ },
93
+ };
94
+ ```
38
95
 
39
- // After creating the mount:
40
- const mount = new ShaderMount(parent, ditheringFragmentShader, uniforms, opts, speed, 0, 2);
41
- window.__BD_SHADER__ = mount;
96
+ ## Tuner Loading (Dev-Only)
97
+
98
+ Add this after the shader script to load the tuner in dev mode:
99
+ ```html
100
+ <script type="module">
101
+ if (import.meta.env.DEV) {
102
+ const s = document.createElement('script');
103
+ s.src = '/bluedither/bluedither-tuner-inject.js';
104
+ document.body.appendChild(s);
105
+ }
106
+ </script>
42
107
  ```
43
108
 
44
- Map shader tokens to uniforms as shown in `template/index.html`'s existing `<script type="module">` block.
109
+ For non-Vite vanilla projects (opened via file:// or a simple HTTP server), use localhost detection instead:
110
+ ```html
111
+ <script type="module">
112
+ if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
113
+ const s = document.createElement('script');
114
+ s.src = '/bluedither/bluedither-tuner-inject.js';
115
+ document.body.appendChild(s);
116
+ }
117
+ </script>
118
+ ```
119
+
120
+ ## CSS Custom Properties
121
+
122
+ The CSS variable names MUST match exactly what the tuner expects:
123
+
124
+ **Colors:** `--bd-bg`, `--bd-primary`, `--bd-text`, `--bd-cta-bg`, `--bd-cta-text`
125
+ **Typography:** `--bd-font-primary`, `--bd-font-secondary`, `--bd-font-primary-weight`, `--bd-font-secondary-weight`, `--bd-headline-size`, `--bd-headline-lh`, `--bd-sub-size`, `--bd-sub-lh`, `--bd-sub-transform`, `--bd-logo-size`, `--bd-logo-lh`, `--bd-nav-size`, `--bd-nav-lh`, `--bd-cta-size`, `--bd-cta-lh`
126
+ **Spacing:** `--bd-header-px`, `--bd-header-py`, `--bd-hero-pt`, `--bd-hero-pb`, `--bd-hero-px`, `--bd-nav-gap`, `--bd-cta-px`, `--bd-cta-py`, `--bd-cta-radius`
127
+ **Opacity:** `--bd-nav-opacity`
128
+
129
+ Using different variable names will break live updates from the tuner.
45
130
 
46
131
  ## File Output
47
132
 
@@ -2,6 +2,40 @@
2
2
 
3
3
  When the target project uses **Vue** (detected via `vue` in package.json dependencies), follow these patterns.
4
4
 
5
+ ## CRITICAL: File Placement Rules
6
+
7
+ **In `src/` (importable source files):**
8
+ - All `.vue` component files
9
+ - `bluedither.css` — CSS custom properties + rules
10
+ - `paper-shaders-bundle.js` — copied from `bluedither/shaders/`
11
+ - Any other JS modules that are `import`-ed
12
+
13
+ **In `public/bluedither/` (static assets loaded at runtime):**
14
+ - `bluedither-tuner.js` — tuner panel
15
+ - `bluedither-tuner.css` — tuner styles
16
+ - `bluedither-tuner-inject.js` — tuner loader
17
+ - `tokens.json` — design tokens (read at runtime by tuner)
18
+ - `tokens.defaults.json` — default tokens
19
+ - `dev-middleware.js` — Vite commit endpoint
20
+
21
+ **NEVER import JS files from `public/` — Vite will error.** Shader files MUST be in `src/`.
22
+
23
+ Copy shader files during generation:
24
+ ```bash
25
+ cp bluedither/shaders/paper-shaders-bundle.js src/paper-shaders-bundle.js
26
+ ```
27
+
28
+ Copy tuner assets to public:
29
+ ```bash
30
+ mkdir -p public/bluedither
31
+ cp bluedither/bluedither-tuner.js public/bluedither/
32
+ cp bluedither/bluedither-tuner.css public/bluedither/
33
+ cp bluedither/bluedither-tuner-inject.js public/bluedither/
34
+ cp bluedither/tokens.json public/bluedither/
35
+ cp bluedither/tokens.defaults.json public/bluedither/
36
+ cp bluedither/dev-middleware.js public/bluedither/
37
+ ```
38
+
5
39
  ## Detection
6
40
 
7
41
  Vue is detected when `package.json` contains `vue` in `dependencies` or `devDependencies`. Also check for:
@@ -10,14 +44,26 @@ Vue is detected when `package.json` contains `vue` in `dependencies` or `devDepe
10
44
 
11
45
  ## Component Structure
12
46
 
13
- Generate Vue Single File Components (SFCs) using `<script setup>` syntax (Vue 3):
47
+ Generate Vue Single File Components (SFCs) using `<script setup>` syntax (Vue 3).
48
+
49
+ ### `src/App.vue` (or modify existing)
50
+ Root component wrapping the full layout. Loads tuner in dev mode.
14
51
 
15
- ### `BlueDitherTheme.vue`
16
- Root component:
17
52
  ```vue
18
53
  <script setup>
19
- import BlueDitherHeader from './BlueDitherHeader.vue';
20
- import BlueDitherHero from './BlueDitherHero.vue';
54
+ import { onMounted } from 'vue';
55
+ import BlueDitherHero from './components/BlueDitherHero.vue';
56
+ import BlueDitherHeader from './components/BlueDitherHeader.vue';
57
+ import './bluedither.css';
58
+
59
+ // Load tuner in dev mode only
60
+ onMounted(() => {
61
+ if (import.meta.env.DEV) {
62
+ const s = document.createElement('script');
63
+ s.src = '/bluedither/bluedither-tuner-inject.js';
64
+ document.body.appendChild(s);
65
+ }
66
+ });
21
67
  </script>
22
68
 
23
69
  <template>
@@ -26,17 +72,16 @@ import BlueDitherHero from './BlueDitherHero.vue';
26
72
  <BlueDitherHeader />
27
73
  </div>
28
74
  </template>
29
-
30
- <style src="./bluedither.css"></style>
31
75
  ```
32
76
 
33
- ### `BlueDitherHeader.vue`
77
+ ### `src/components/BlueDitherHeader.vue`
34
78
  Navbar with logo, nav items, CTA:
35
79
  ```vue
36
80
  <script setup>
81
+ // Content from tokens.json content section
82
+ const companyName = 'COMPANYLOGO';
37
83
  const navItems = ['HOME', 'FEATURES', 'FAQ', 'CONTACT'];
38
84
  const ctaText = 'SIGN UP';
39
- const companyName = 'COMPANYLOGO';
40
85
  </script>
41
86
 
42
87
  <template>
@@ -54,25 +99,100 @@ const companyName = 'COMPANYLOGO';
54
99
  </template>
55
100
  ```
56
101
 
57
- ### `BlueDitherHero.vue`
58
- Hero section with shader initialization using `onMounted`:
102
+ ### `src/components/BlueDitherHero.vue`
103
+ Hero section with shader initialization. **CRITICAL:** The `window.__BD_SHADER__` wrapper is required — the tuner calls `updateParams()` on it, but ShaderMount only has `setUniforms()` and `setSpeed()`. You MUST create the translation wrapper.
104
+
59
105
  ```vue
60
106
  <script setup>
61
107
  import { ref, onMounted, onUnmounted } from 'vue';
62
108
 
109
+ // Content from tokens.json
110
+ const headline = 'AN ABSOLUTELY\nFANTASTIC HEADLINE.';
111
+ const subHeadline = "Here's a fantastic sub-headline that is sure to impressive your friends.";
112
+
113
+ // Shader params from tokens.json
114
+ const SHADER_PARAMS = {
115
+ colorFront: '#005A6A',
116
+ colorBack: '#00000000',
117
+ shape: 'ripple',
118
+ type: '2x2',
119
+ size: 4.2,
120
+ scale: 1,
121
+ speed: 0.41,
122
+ rotation: 180,
123
+ };
124
+
63
125
  const shaderParent = ref(null);
64
126
  let shaderMount = null;
65
127
 
66
128
  onMounted(async () => {
67
- const { ShaderMount, ditheringFragmentShader, getShaderColorFromString } = await import('./paper-shaders-bundle.js');
68
- shaderMount = new ShaderMount(shaderParent.value, ditheringFragmentShader, uniforms, opts, speed, 0, 2);
69
- window.__BD_SHADER__ = shaderMount;
129
+ if (!shaderParent.value) return;
130
+ const {
131
+ ShaderMount,
132
+ ditheringFragmentShader,
133
+ DitheringShapes,
134
+ DitheringTypes,
135
+ getShaderColorFromString,
136
+ } = await import('../paper-shaders-bundle.js');
137
+
138
+ shaderMount = new ShaderMount(
139
+ shaderParent.value,
140
+ ditheringFragmentShader,
141
+ {
142
+ u_colorFront: getShaderColorFromString(SHADER_PARAMS.colorFront),
143
+ u_colorBack: getShaderColorFromString(SHADER_PARAMS.colorBack),
144
+ u_shape: DitheringShapes[SHADER_PARAMS.shape] ?? DitheringShapes.warp,
145
+ u_type: DitheringTypes[SHADER_PARAMS.type] ?? DitheringTypes['2x2'],
146
+ u_pxSize: SHADER_PARAMS.size,
147
+ u_scale: SHADER_PARAMS.scale,
148
+ u_rotation: SHADER_PARAMS.rotation,
149
+ u_originX: 0.5,
150
+ u_originY: 0.5,
151
+ u_worldWidth: 0,
152
+ u_worldHeight: 0,
153
+ u_fit: 0,
154
+ u_offsetX: 0,
155
+ u_offsetY: 0,
156
+ },
157
+ { alpha: true, premultipliedAlpha: false },
158
+ SHADER_PARAMS.speed,
159
+ 0,
160
+ 2
161
+ );
162
+
163
+ // CRITICAL: expose wrapper with updateParams for the tuner
164
+ // ShaderMount has setUniforms() and setSpeed() — NOT updateParams()
165
+ // The tuner calls updateParams() with token names, this translates to uniform names
166
+ window.__BD_SHADER__ = {
167
+ updateParams(params) {
168
+ const uniforms = {};
169
+ if (params.colorFront !== undefined)
170
+ uniforms.u_colorFront = getShaderColorFromString(params.colorFront);
171
+ if (params.colorBack !== undefined)
172
+ uniforms.u_colorBack = getShaderColorFromString(params.colorBack);
173
+ if (params.shape !== undefined)
174
+ uniforms.u_shape = DitheringShapes[params.shape] ?? DitheringShapes.warp;
175
+ if (params.type !== undefined)
176
+ uniforms.u_type = DitheringTypes[params.type] ?? DitheringTypes['2x2'];
177
+ if (params.size !== undefined) uniforms.u_pxSize = params.size;
178
+ if (params.scale !== undefined) uniforms.u_scale = params.scale;
179
+ if (params.rotation !== undefined)
180
+ uniforms.u_rotation = parseFloat(params.rotation) || 180;
181
+ if (Object.keys(uniforms).length > 0) shaderMount.setUniforms(uniforms);
182
+ if (params.speed !== undefined) shaderMount.setSpeed(params.speed);
183
+ },
184
+ dispose() {
185
+ shaderMount?.dispose();
186
+ },
187
+ };
70
188
  });
71
189
 
72
190
  onUnmounted(() => {
73
- shaderMount?.dispose();
191
+ window.__BD_SHADER__?.dispose();
74
192
  window.__BD_SHADER__ = null;
75
193
  });
194
+
195
+ const headlineLines = headline.split('\n');
76
196
  </script>
77
197
 
78
198
  <template>
@@ -80,14 +200,40 @@ onUnmounted(() => {
80
200
  <div class="bd-hero-inner">
81
201
  <div ref="shaderParent" class="bd-shader-layer"></div>
82
202
  <div class="bd-hero-content">
83
- <div class="bd-headline">...</div>
84
- <div class="bd-subheadline">...</div>
203
+ <div class="bd-headline">
204
+ <template v-for="(line, i) in headlineLines" :key="i">
205
+ <br v-if="i > 0" />{{ line }}
206
+ </template>
207
+ </div>
208
+ <div class="bd-subheadline">{{ subHeadline }}</div>
85
209
  </div>
86
210
  </div>
87
211
  </div>
88
212
  </template>
89
213
  ```
90
214
 
215
+ Note: The shader uses `ShaderMount` directly from `paper-shaders-bundle.js` — do NOT create a separate shader wrapper module. Import `ShaderMount`, `ditheringFragmentShader`, `DitheringShapes`, `DitheringTypes`, and `getShaderColorFromString` directly.
216
+
217
+ The `window.__BD_SHADER__` wrapper is **required** — the tuner calls `updateParams()` on it for live shader editing. The ShaderMount instance has `setUniforms()` and `setSpeed()` methods. The wrapper translates token names to uniform names (e.g., `colorFront` → `u_colorFront`).
218
+
219
+ ### `src/bluedither.css`
220
+ All CSS custom properties and `.bd-*` class rules. See `bluedither/template/index.html` for the exact variable names and class styles. The CSS variable names MUST match exactly:
221
+
222
+ **Colors:** `--bd-bg`, `--bd-primary`, `--bd-text`, `--bd-cta-bg`, `--bd-cta-text`
223
+ **Typography:** `--bd-font-primary`, `--bd-font-secondary`, `--bd-font-primary-weight`, `--bd-font-secondary-weight`, `--bd-headline-size`, `--bd-headline-lh`, `--bd-sub-size`, `--bd-sub-lh`, `--bd-sub-transform`, `--bd-logo-size`, `--bd-logo-lh`, `--bd-nav-size`, `--bd-nav-lh`, `--bd-cta-size`, `--bd-cta-lh`
224
+ **Spacing:** `--bd-header-px`, `--bd-header-py`, `--bd-hero-pt`, `--bd-hero-pb`, `--bd-hero-px`, `--bd-nav-gap`, `--bd-cta-px`, `--bd-cta-py`, `--bd-cta-radius`
225
+ **Opacity:** `--bd-nav-opacity`
226
+
227
+ Using different variable names will break live updates from the tuner.
228
+
229
+ ## Vite Config
230
+
231
+ The `npx bluedither install` command auto-wires the Vite plugin. If it wasn't auto-wired, add manually:
232
+ ```js
233
+ import { bluedither } from './bluedither/dev-middleware.js'
234
+ export default defineConfig({ plugins: [vue(), bluedither()] })
235
+ ```
236
+
91
237
  ## TypeScript
92
238
 
93
239
  If TypeScript is detected, add `lang="ts"` to `<script setup lang="ts">`. Type the template ref:
@@ -95,12 +241,26 @@ If TypeScript is detected, add `lang="ts"` to `<script setup lang="ts">`. Type t
95
241
  const shaderParent = ref<HTMLDivElement | null>(null);
96
242
  ```
97
243
 
244
+ ## Nuxt
245
+
246
+ If Nuxt is detected:
247
+ - Place components in `components/` directory (auto-imported)
248
+ - Use `useHead()` for font links
249
+ - Use `process.client` for shader initialization
250
+ - Use `process.dev` for tuner loading
251
+
252
+ ## Content Handling
253
+
254
+ - Inject content tokens directly as Vue template text
255
+ - Headline newlines: split and use `v-for` with `<br>` (see example above)
256
+ - Nav items: `v-for="item in navItems"`
257
+
98
258
  ## File Output
99
259
 
100
260
  | File | Contents |
101
261
  |------|----------|
102
- | `BlueDitherTheme.vue` | Root layout SFC |
103
- | `BlueDitherHeader.vue` | Navbar SFC |
104
- | `BlueDitherHero.vue` | Hero + shader SFC |
105
- | `bluedither.css` | CSS custom properties + rules |
106
- | `paper-shaders-bundle.js` | Shader library (copied) |
262
+ | `src/App.vue` | Root layout SFC (or modify existing) |
263
+ | `src/components/BlueDitherHeader.vue` | Navbar SFC |
264
+ | `src/components/BlueDitherHero.vue` | Hero + shader SFC |
265
+ | `src/bluedither.css` | CSS custom properties + rules |
266
+ | `src/paper-shaders-bundle.js` | Shader library (copied from bluedither/shaders/) |