bluedither 1.0.12 → 1.0.14
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.
- package/cli/commands/install.js +5 -0
- package/dist/bluedither-tuner.js +15 -15
- package/fine-tuner/dev-middleware.js +110 -0
- package/fine-tuner/inject.js +3 -28
- package/fine-tuner/tuner.js +88 -32
- package/package.json +1 -1
- package/skill.md +2 -8
- package/theme/generators/react.md +3 -2
- package/theme/generators/svelte.md +2 -0
- package/theme/generators/vanilla.md +5 -1
- package/theme/generators/vue.md +2 -0
package/cli/commands/install.js
CHANGED
|
@@ -103,6 +103,10 @@ export default async function install(args) {
|
|
|
103
103
|
if (existsSync(injectPath)) {
|
|
104
104
|
filesToCopy.push(['fine-tuner/inject.js', 'bluedither-tuner-inject.js']);
|
|
105
105
|
}
|
|
106
|
+
const middlewarePath = resolve(BLUEDITHER_ROOT, 'fine-tuner', 'dev-middleware.js');
|
|
107
|
+
if (existsSync(middlewarePath)) {
|
|
108
|
+
filesToCopy.push(['fine-tuner/dev-middleware.js', 'dev-middleware.js']);
|
|
109
|
+
}
|
|
106
110
|
|
|
107
111
|
let copied = 0;
|
|
108
112
|
for (const [src, dest] of filesToCopy) {
|
|
@@ -203,6 +207,7 @@ async function installFromRegistry(slug, targetDir) {
|
|
|
203
207
|
['dist/bluedither-tuner.js', 'bluedither-tuner.js'],
|
|
204
208
|
['fine-tuner/tuner.css', 'bluedither-tuner.css'],
|
|
205
209
|
['fine-tuner/inject.js', 'bluedither-tuner-inject.js'],
|
|
210
|
+
['fine-tuner/dev-middleware.js', 'dev-middleware.js'],
|
|
206
211
|
];
|
|
207
212
|
for (const [src, dest] of tunerFiles) {
|
|
208
213
|
const srcPath = resolve(BLUEDITHER_ROOT, src);
|
package/dist/bluedither-tuner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
(()=>{var
|
|
1
|
+
(()=>{var p=structuredClone(window.__BD_TOKENS__||JSON.parse(document.querySelector('script[type="application/json"][data-bluedither-tokens]')?.textContent||"{}")),N=structuredClone(window.__BD_DEFAULTS__||JSON.parse(document.querySelector('script[type="application/json"][data-bluedither-defaults]')?.textContent||"{}")),j=!!(window.__BD_TOKENS__&&window.__BD_DEFAULTS__)||!!window.__BD_TUNER_SERVER_MODE__,$=["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"],H=["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"],A=[...new Set([...$,...H])].sort();function v(n,t,e){let i=t.split("."),o=n;for(let d=0;d<i.length-1;d++)o=o[i[d]];o[i[i.length-1]]=e}function E(n,t){return t.split(".").reduce((e,i)=>e?.[i],n)}function k(n,t){document.documentElement.style.setProperty(n,t)}function S(n,t){let e=window.__BD_SHADER__;e&&e.updateParams({[n]:t})}function U(n){let t=p.layout.designWidth,e=n/16,i=n/t*100;return`clamp(${(e*.55).toFixed(4)}rem, ${i.toFixed(4)}vw, ${e.toFixed(4)}rem)`}function R(n){let t="bd-gf-"+n.replace(/\s+/g,"-").toLowerCase();if(document.getElementById(t))return;let e=document.createElement("link");e.id=t,e.rel="stylesheet",e.href=`https://fonts.googleapis.com/css2?family=${encodeURIComponent(n)}:wght@400;700&display=swap`,document.head.appendChild(e)}var x=document.createElement("div");x.id="bd-tuner";var M=document.createElement("button");M.id="bd-tuner-toggle";M.textContent="Tuner";M.onclick=()=>x.classList.remove("collapsed");document.body.appendChild(x);document.body.appendChild(M);x.innerHTML=`
|
|
2
2
|
<div class="bd-tuner-title">
|
|
3
3
|
<span>BlueDither Tuner</span>
|
|
4
4
|
<div style="display:flex;gap:8px;align-items:center;">
|
|
@@ -6,37 +6,37 @@
|
|
|
6
6
|
<button id="bd-tuner-close" title="Close">×</button>
|
|
7
7
|
</div>
|
|
8
8
|
</div>
|
|
9
|
-
`;x.querySelector("#bd-tuner-close").onclick=()=>x.classList.add("collapsed");x.querySelector("#bd-tuner-reset").onclick=()=>{confirm("Reset all tokens to defaults?")&&(Object.assign(
|
|
9
|
+
`;x.querySelector("#bd-tuner-close").onclick=()=>x.classList.add("collapsed");x.querySelector("#bd-tuner-reset").onclick=async()=>{confirm("Reset all tokens to defaults?")&&(Object.assign(p,structuredClone(N)),await I(),location.reload())};function T(n){let t=document.createElement("div");return t.className="bd-tuner-section",t.innerHTML=`<div class="bd-tuner-section-label">${n}</div>`,x.appendChild(t),t}function C(n,t,e,i,o){let d=E(p,e)||"#000000",a=document.createElement("div");a.className="bd-tuner-row",a.innerHTML=`
|
|
10
10
|
<span class="bd-tuner-label">${t}</span>
|
|
11
|
-
<span class="bd-tuner-input"><input type="color" value="${
|
|
12
|
-
<span class="bd-tuner-value">${
|
|
13
|
-
`;let
|
|
11
|
+
<span class="bd-tuner-input"><input type="color" value="${d.substring(0,7)}"></span>
|
|
12
|
+
<span class="bd-tuner-value">${d.substring(0,7)}</span>
|
|
13
|
+
`;let s=a.querySelector("input"),c=a.querySelector(".bd-tuner-value");return s.addEventListener("input",l=>{let m=l.target.value;v(p,e,m),c.textContent=m,i&&k(i,m),o&&o(m)}),n.appendChild(a),{setValue(l){v(p,e,l),s.value=l.substring(0,7),c.textContent=l.substring(0,7),i&&k(i,l)}}}function f(n,t,e,i,o,d,a){let s=E(p,e),c=document.createElement("div");c.className="bd-tuner-row bd-tuner-row-stacked",c.innerHTML=`
|
|
14
14
|
<div class="bd-tuner-row-top">
|
|
15
15
|
<span class="bd-tuner-label">${t}</span>
|
|
16
16
|
<div class="bd-tuner-px-input-wrap">
|
|
17
|
-
<input type="number" class="bd-tuner-px-num" value="${
|
|
17
|
+
<input type="number" class="bd-tuner-px-num" value="${s}" min="${i}" max="${o}" step="${d}">
|
|
18
18
|
<span class="bd-tuner-unit">px</span>
|
|
19
19
|
</div>
|
|
20
20
|
</div>
|
|
21
|
-
<input type="range" class="bd-tuner-slider" min="${
|
|
22
|
-
`;let c
|
|
21
|
+
<input type="range" class="bd-tuner-slider" min="${i}" max="${o}" step="${d}" value="${s}">
|
|
22
|
+
`;let l=c.querySelector(".bd-tuner-px-num"),m=c.querySelector(".bd-tuner-slider");function u(r){if(r=Math.max(i,Math.min(o,r)),v(p,e,r),l.value=r,m.value=r,a){let w=U(r);(Array.isArray(a)?a:[a]).forEach(g=>k(g,w))}}l.addEventListener("input",r=>u(parseFloat(r.target.value)||0)),m.addEventListener("input",r=>u(parseFloat(r.target.value))),l.addEventListener("keydown",r=>{if(r.key==="ArrowUp"||r.key==="ArrowDown"){r.preventDefault();let w=r.shiftKey?10:1,g=(r.key==="ArrowUp"?d:-d)*w;u(parseFloat(l.value)+g)}}),n.appendChild(c)}function P(n,t,e,i,o,d,a){let s=E(p,e),c=document.createElement("div");c.className="bd-tuner-row bd-tuner-row-stacked",c.innerHTML=`
|
|
23
23
|
<div class="bd-tuner-row-top">
|
|
24
24
|
<span class="bd-tuner-label">${t}</span>
|
|
25
|
-
<span class="bd-tuner-value">${
|
|
25
|
+
<span class="bd-tuner-value">${s}</span>
|
|
26
26
|
</div>
|
|
27
|
-
<input type="range" class="bd-tuner-slider" min="${
|
|
28
|
-
`;let c
|
|
27
|
+
<input type="range" class="bd-tuner-slider" min="${i}" max="${o}" step="${d}" value="${s}">
|
|
28
|
+
`;let l=c.querySelector(".bd-tuner-slider"),m=c.querySelector(".bd-tuner-value");l.addEventListener("input",u=>{let r=parseFloat(u.target.value);v(p,e,r),m.textContent=r,a&&a(r)}),n.appendChild(c)}function B(n,t,e,i){let o=E(p,e),d=document.createElement("div");d.className="bd-tuner-row",d.innerHTML=`
|
|
29
29
|
<span class="bd-tuner-label">${t}</span>
|
|
30
30
|
<span class="bd-tuner-input bd-tuner-font-input">
|
|
31
|
-
<input type="text" class="bd-tuner-font-search" value="${
|
|
31
|
+
<input type="text" class="bd-tuner-font-search" value="${o}" placeholder="Search fonts...">
|
|
32
32
|
<div class="bd-tuner-font-dropdown"></div>
|
|
33
33
|
</span>
|
|
34
|
-
`;let
|
|
34
|
+
`;let a=d.querySelector(".bd-tuner-font-search"),s=d.querySelector(".bd-tuner-font-dropdown"),c=m=>$.some(u=>u.toLowerCase()===m.toLowerCase());function l(m=""){let u=A.filter(r=>r.toLowerCase().includes(m.toLowerCase())).slice(0,20);s.innerHTML=u.map(r=>{let w=c(r)?' <span style="opacity:0.4;font-size:9px">SYSTEM</span>':"";return`<div class="bd-tuner-font-option" data-font="${r}" style="font-family:'${r}',system-ui">${r}${w}</div>`}).join(""),u.filter(r=>!c(r)).forEach(R),s.querySelectorAll(".bd-tuner-font-option").forEach(r=>{r.addEventListener("mousedown",w=>{w.preventDefault();let g=r.dataset.font;a.value=g,v(p,e,g),c(g)||R(g),k(i,`"${g}", 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&&(v(p,e,u),c(u)||R(u),k(i,`"${u}", system-ui, sans-serif`),s.classList.remove("open"),a.blur())}}),n.appendChild(d)}function D(n,t,e,i,o){let d=E(p,e),a=document.createElement("div");a.className="bd-tuner-row bd-tuner-row-stacked",a.innerHTML=`
|
|
35
35
|
<span class="bd-tuner-label">${t}</span>
|
|
36
36
|
<div class="bd-tuner-segmented">
|
|
37
|
-
${
|
|
37
|
+
${i.map(s=>`<button class="bd-tuner-seg-btn${s===d?" active":""}" data-val="${s}">${s}</button>`).join("")}
|
|
38
38
|
</div>
|
|
39
|
-
`,
|
|
39
|
+
`,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;v(p,e,c),o&&o(c)})}),n.appendChild(a)}var _=T("Colors");C(_,"Background","colors.background","--bd-color-background");var O;C(_,"Primary","colors.primary","--bd-color-primary",n=>{O.setValue(n),S("colorFront",n)});C(_,"Text","colors.text","--bd-color-text");C(_,"CTA Background","colors.ctaBackground","--bd-color-cta-bg");C(_,"CTA Text","colors.ctaText","--bd-color-cta-text");O=C(_,"Shader Front","colors.shaderFront",null,n=>S("colorFront",n));var y=T("Typography");B(y,"Primary Font","typography.primaryFont","--bd-font-primary");B(y,"Secondary Font","typography.secondaryFont","--bd-font-secondary");f(y,"Headline Size","typography.headline.referencePx",32,300,1,"--bd-headline-size");f(y,"Headline LH","typography.headline.lineHeightPx",24,280,1,"--bd-headline-lh");f(y,"Sub Size","typography.subHeadline.referencePx",10,48,1,"--bd-subheadline-size");f(y,"Sub LH","typography.subHeadline.lineHeightPx",12,80,1,"--bd-subheadline-lh");f(y,"Logo Size","typography.logo.referencePx",12,80,1,"--bd-logo-size");f(y,"Nav Size","typography.navItem.referencePx",10,48,1,"--bd-nav-size");var h=T("Spacing");f(h,"Header Pad X","spacing.headerPaddingX",0,80,1,"--bd-header-px");f(h,"Header Pad Y","spacing.headerPaddingY",0,60,1,"--bd-header-py");f(h,"Hero Pad Top","spacing.heroPaddingTop",0,120,1,"--bd-hero-pt");f(h,"Hero Pad Bottom","spacing.heroPaddingBottom",0,120,1,"--bd-hero-pb");f(h,"Hero Pad X","spacing.heroPaddingX",0,120,1,"--bd-hero-px");f(h,"Nav Gap","spacing.navGap",0,100,1,"--bd-nav-gap");f(h,"CTA Pad X","spacing.ctaPaddingX",0,60,1,"--bd-cta-px");f(h,"CTA Pad Y","spacing.ctaPaddingY",0,30,1,"--bd-cta-py");f(h,"CTA Radius","spacing.ctaBorderRadius",0,32,1,"--bd-cta-radius");var L=T("Shader");D(L,"Shape","shader.shape",["warp","simplex","dots","wave","ripple","swirl","sphere"],n=>S("shape",n));D(L,"Dither Type","shader.type",["random","2x2","4x4","8x8"],n=>S("type",n));P(L,"Speed","shader.speed",0,2,.01,n=>S("speed",n));P(L,"Scale","shader.scale",.1,5,.01,n=>S("scale",n));P(L,"Dither Size","shader.size",.5,10,.1,n=>S("size",n));var q=T("Opacity");P(q,"Nav Links","opacity.navLinks",0,1,.01,n=>k("--bd-nav-opacity",n));var b=document.createElement("button");b.id="bd-tuner-commit";b.textContent="Commit Changes";var F=null;async function I(){let n=JSON.stringify(p,null,2);try{let o=await fetch("/__bluedither/commit",{method:"POST",headers:{"Content-Type":"application/json"},body:n});if(o.ok&&(await o.json()).ok)return"saved"}catch{}try{let o=await fetch("http://localhost:3344/commit",{method:"POST",headers:{"Content-Type":"application/json"},body:n});if(o.ok&&(await o.json()).ok)return"saved"}catch{}if(j)try{let o=await fetch("/commit",{method:"POST",headers:{"Content-Type":"application/json"},body:n});if(o.ok&&(await o.json()).ok)return"saved"}catch{}if(window.showSaveFilePicker)try{F||(F=await window.showSaveFilePicker({suggestedName:"tokens.json",types:[{description:"JSON",accept:{"application/json":[".json"]}}]}));let o=await F.createWritable();return await o.write(n),await o.close(),"saved"}catch(o){if(o.name==="AbortError")return"cancelled";F=null}let t=new Blob([n],{type:"application/json"}),e=URL.createObjectURL(t),i=document.createElement("a");return i.href=e,i.download="tokens.json",i.click(),URL.revokeObjectURL(e),"downloaded"}b.onclick=async()=>{b.textContent="Exporting...",b.disabled=!0;try{if(window.__BD_SERVER_MODE__){let t=await(await fetch("/commit",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(p,null,2)})).json();if(!t.ok)throw new Error(t.error);b.textContent="Committed!"}else{let n=new Blob([JSON.stringify(p,null,2)],{type:"application/json"}),t=URL.createObjectURL(n),e=document.createElement("a");e.href=t,e.download="tokens.json",e.click(),URL.revokeObjectURL(t),b.textContent="Downloaded!"}b.classList.add("success"),setTimeout(()=>{b.textContent=window.__BD_SERVER_MODE__?"Commit Changes":"Export Tokens",b.classList.remove("success"),b.disabled=!1},2e3)}catch(n){b.textContent="Error: "+n.message,b.disabled=!1,setTimeout(()=>{b.textContent=window.__BD_SERVER_MODE__?"Commit Changes":"Export Tokens"},3e3)}};window.__BD_SERVER_MODE__||(b.textContent="Export Tokens");x.appendChild(b);var z=document.createElement("style");z.textContent=`/* =============================================
|
|
40
40
|
BlueDither Fine-Tuner \u2014 Overlay Panel v2
|
|
41
41
|
============================================= */
|
|
42
42
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BlueDither Dev Middleware
|
|
3
|
+
*
|
|
4
|
+
* Universal middleware that handles POST /___bluedither/commit
|
|
5
|
+
* to write tokens.json to disk. Works with any Node.js dev server.
|
|
6
|
+
*
|
|
7
|
+
* Exports:
|
|
8
|
+
* bluedither() — Vite plugin
|
|
9
|
+
* blueditherMiddleware() — Express/Connect middleware
|
|
10
|
+
* blueditherHandler() — Raw Node http handler (for custom servers)
|
|
11
|
+
*
|
|
12
|
+
* The grab-theme command auto-wires the right export for the detected framework.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { writeFileSync, existsSync } from 'fs';
|
|
16
|
+
import { resolve, dirname } from 'path';
|
|
17
|
+
|
|
18
|
+
const ENDPOINT = '/__bluedither/commit';
|
|
19
|
+
|
|
20
|
+
function findTokensPath() {
|
|
21
|
+
// Search from cwd for bluedither/tokens.json
|
|
22
|
+
const cwd = process.cwd();
|
|
23
|
+
const paths = [
|
|
24
|
+
resolve(cwd, 'bluedither', 'tokens.json'),
|
|
25
|
+
resolve(cwd, 'public', 'bluedither', 'tokens.json'),
|
|
26
|
+
resolve(cwd, 'theme', 'tokens.json'),
|
|
27
|
+
];
|
|
28
|
+
for (const p of paths) {
|
|
29
|
+
if (existsSync(p)) return p;
|
|
30
|
+
}
|
|
31
|
+
// Default to bluedither/tokens.json even if it doesn't exist yet
|
|
32
|
+
return resolve(cwd, 'bluedither', 'tokens.json');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function handleRequest(req, res) {
|
|
36
|
+
// CORS preflight
|
|
37
|
+
if (req.method === 'OPTIONS' && req.url === ENDPOINT) {
|
|
38
|
+
res.writeHead(204, {
|
|
39
|
+
'Access-Control-Allow-Origin': '*',
|
|
40
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
41
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
42
|
+
});
|
|
43
|
+
res.end();
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (req.method === 'POST' && req.url === ENDPOINT) {
|
|
48
|
+
let body = '';
|
|
49
|
+
req.on('data', chunk => { body += chunk; });
|
|
50
|
+
req.on('end', () => {
|
|
51
|
+
try {
|
|
52
|
+
// Validate it's JSON
|
|
53
|
+
JSON.parse(body);
|
|
54
|
+
const tokensPath = findTokensPath();
|
|
55
|
+
writeFileSync(tokensPath, body);
|
|
56
|
+
res.writeHead(200, {
|
|
57
|
+
'Content-Type': 'application/json',
|
|
58
|
+
'Access-Control-Allow-Origin': '*',
|
|
59
|
+
});
|
|
60
|
+
res.end('{"ok":true}');
|
|
61
|
+
} catch (e) {
|
|
62
|
+
res.writeHead(500, {
|
|
63
|
+
'Content-Type': 'application/json',
|
|
64
|
+
'Access-Control-Allow-Origin': '*',
|
|
65
|
+
});
|
|
66
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Vite plugin — add to vite.config.js:
|
|
77
|
+
* import { bluedither } from './bluedither/dev-middleware.js'
|
|
78
|
+
* export default { plugins: [bluedither()] }
|
|
79
|
+
*/
|
|
80
|
+
export function bluedither() {
|
|
81
|
+
return {
|
|
82
|
+
name: 'bluedither-tuner',
|
|
83
|
+
configureServer(server) {
|
|
84
|
+
server.middlewares.use((req, res, next) => {
|
|
85
|
+
if (!handleRequest(req, res)) next();
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Express/Connect middleware — add to your server:
|
|
93
|
+
* import { blueditherMiddleware } from './bluedither/dev-middleware.js'
|
|
94
|
+
* app.use(blueditherMiddleware())
|
|
95
|
+
*/
|
|
96
|
+
export function blueditherMiddleware() {
|
|
97
|
+
return (req, res, next) => {
|
|
98
|
+
if (!handleRequest(req, res)) next();
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Raw handler for custom Node http servers:
|
|
104
|
+
* import { blueditherHandler } from './bluedither/dev-middleware.js'
|
|
105
|
+
* // In your request handler:
|
|
106
|
+
* if (!blueditherHandler(req, res)) { /* your normal handling */ }
|
|
107
|
+
*/
|
|
108
|
+
export { handleRequest as blueditherHandler };
|
|
109
|
+
|
|
110
|
+
export default bluedither;
|
package/fine-tuner/inject.js
CHANGED
|
@@ -3,13 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Drop this script into ANY page to get the tuner overlay.
|
|
5
5
|
* Reads tokens from bluedither/tokens.json, updates CSS vars live.
|
|
6
|
-
*
|
|
6
|
+
* "Commit Changes" saves via dev middleware → sidecar → File System API → download.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
9
|
* <script src="./bluedither/bluedither-tuner-inject.js"></script>
|
|
10
|
-
*
|
|
11
|
-
* The sidecar save server:
|
|
12
|
-
* npx bluedither tune --save-only
|
|
13
10
|
*/
|
|
14
11
|
|
|
15
12
|
(async function () {
|
|
@@ -38,30 +35,8 @@
|
|
|
38
35
|
window.__BD_TOKENS__ = tokens;
|
|
39
36
|
window.__BD_DEFAULTS__ = defaults;
|
|
40
37
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
let sidecarAvailable = false;
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
const probe = await fetch(`http://localhost:${SIDECAR_PORT}/ping`, { method: 'GET', signal: AbortSignal.timeout(500) });
|
|
47
|
-
sidecarAvailable = probe.ok;
|
|
48
|
-
} catch {
|
|
49
|
-
// Sidecar not running
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Patch the commit mechanism
|
|
53
|
-
if (sidecarAvailable) {
|
|
54
|
-
// Override fetch for /commit to redirect to sidecar
|
|
55
|
-
const origFetch = window.fetch;
|
|
56
|
-
window.fetch = function (url, opts) {
|
|
57
|
-
if (url === '/commit' && opts?.method === 'POST') {
|
|
58
|
-
return origFetch.call(this, `http://localhost:${SIDECAR_PORT}/commit`, opts);
|
|
59
|
-
}
|
|
60
|
-
return origFetch.apply(this, arguments);
|
|
61
|
-
};
|
|
62
|
-
// Signal to tuner that server mode is available
|
|
63
|
-
window.__BD_TUNER_SERVER_MODE__ = true;
|
|
64
|
-
}
|
|
38
|
+
// Signal that we're in injectable mode (tuner will use commitTokens cascade)
|
|
39
|
+
window.__BD_TUNER_SERVER_MODE__ = true;
|
|
65
40
|
|
|
66
41
|
// Load tuner CSS
|
|
67
42
|
try {
|
package/fine-tuner/tuner.js
CHANGED
|
@@ -112,18 +112,11 @@ panel.innerHTML = `
|
|
|
112
112
|
</div>
|
|
113
113
|
`;
|
|
114
114
|
panel.querySelector('#bd-tuner-close').onclick = () => panel.classList.add('collapsed');
|
|
115
|
-
panel.querySelector('#bd-tuner-reset').onclick = () => {
|
|
115
|
+
panel.querySelector('#bd-tuner-reset').onclick = async () => {
|
|
116
116
|
if (!confirm('Reset all tokens to defaults?')) return;
|
|
117
117
|
Object.assign(tokens, structuredClone(defaults));
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
method: 'POST',
|
|
121
|
-
headers: { 'Content-Type': 'application/json' },
|
|
122
|
-
body: JSON.stringify(tokens, null, 2)
|
|
123
|
-
}).then(() => location.reload());
|
|
124
|
-
} else {
|
|
125
|
-
location.reload();
|
|
126
|
-
}
|
|
118
|
+
await commitTokens();
|
|
119
|
+
location.reload();
|
|
127
120
|
};
|
|
128
121
|
|
|
129
122
|
// ── Section builders ──
|
|
@@ -404,42 +397,105 @@ addRange(opacitySection, 'Nav Links', 'opacity.navLinks', 0, 1, 0.01, (v) => set
|
|
|
404
397
|
const commitBtn = document.createElement('button');
|
|
405
398
|
commitBtn.id = 'bd-tuner-commit';
|
|
406
399
|
commitBtn.textContent = 'Commit Changes';
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
400
|
+
|
|
401
|
+
// Try to commit via dev middleware, then sidecar, then File System Access API, then download
|
|
402
|
+
let __fileHandle = null; // Cached File System Access API handle
|
|
403
|
+
|
|
404
|
+
async function commitTokens() {
|
|
405
|
+
const payload = JSON.stringify(tokens, null, 2);
|
|
406
|
+
|
|
407
|
+
// Strategy 1: Dev middleware (Vite plugin, Express middleware, etc.)
|
|
412
408
|
try {
|
|
413
|
-
|
|
409
|
+
const res = await fetch('/__bluedither/commit', {
|
|
410
|
+
method: 'POST',
|
|
411
|
+
headers: { 'Content-Type': 'application/json' },
|
|
412
|
+
body: payload,
|
|
413
|
+
});
|
|
414
|
+
if (res.ok) {
|
|
415
|
+
const data = await res.json();
|
|
416
|
+
if (data.ok) return 'saved';
|
|
417
|
+
}
|
|
418
|
+
} catch {}
|
|
419
|
+
|
|
420
|
+
// Strategy 2: Sidecar on port 3344 (if running)
|
|
421
|
+
try {
|
|
422
|
+
const res = await fetch('http://localhost:3344/commit', {
|
|
423
|
+
method: 'POST',
|
|
424
|
+
headers: { 'Content-Type': 'application/json' },
|
|
425
|
+
body: payload,
|
|
426
|
+
});
|
|
427
|
+
if (res.ok) {
|
|
428
|
+
const data = await res.json();
|
|
429
|
+
if (data.ok) return 'saved';
|
|
430
|
+
}
|
|
431
|
+
} catch {}
|
|
432
|
+
|
|
433
|
+
// Strategy 3: Legacy /commit endpoint (BlueDither's own dev server)
|
|
434
|
+
if (__serverMode) {
|
|
435
|
+
try {
|
|
414
436
|
const res = await fetch('/commit', {
|
|
415
437
|
method: 'POST',
|
|
416
438
|
headers: { 'Content-Type': 'application/json' },
|
|
417
|
-
body:
|
|
439
|
+
body: payload,
|
|
418
440
|
});
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
441
|
+
if (res.ok) {
|
|
442
|
+
const data = await res.json();
|
|
443
|
+
if (data.ok) return 'saved';
|
|
444
|
+
}
|
|
445
|
+
} catch {}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Strategy 4: File System Access API (Chrome/Edge — no server needed)
|
|
449
|
+
if (window.showSaveFilePicker) {
|
|
450
|
+
try {
|
|
451
|
+
if (!__fileHandle) {
|
|
452
|
+
__fileHandle = await window.showSaveFilePicker({
|
|
453
|
+
suggestedName: 'tokens.json',
|
|
454
|
+
types: [{ description: 'JSON', accept: { 'application/json': ['.json'] } }],
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
const writable = await __fileHandle.createWritable();
|
|
458
|
+
await writable.write(payload);
|
|
459
|
+
await writable.close();
|
|
460
|
+
return 'saved';
|
|
461
|
+
} catch (e) {
|
|
462
|
+
if (e.name === 'AbortError') return 'cancelled';
|
|
463
|
+
__fileHandle = null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Strategy 5: Download as last resort
|
|
468
|
+
const blob = new Blob([payload], { type: 'application/json' });
|
|
469
|
+
const url = URL.createObjectURL(blob);
|
|
470
|
+
const a = document.createElement('a');
|
|
471
|
+
a.href = url;
|
|
472
|
+
a.download = 'tokens.json';
|
|
473
|
+
a.click();
|
|
474
|
+
URL.revokeObjectURL(url);
|
|
475
|
+
return 'downloaded';
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
commitBtn.onclick = async () => {
|
|
479
|
+
commitBtn.textContent = 'Saving...';
|
|
480
|
+
commitBtn.disabled = true;
|
|
481
|
+
try {
|
|
482
|
+
const result = await commitTokens();
|
|
483
|
+
if (result === 'cancelled') {
|
|
484
|
+
commitBtn.textContent = 'Commit Changes';
|
|
485
|
+
commitBtn.disabled = false;
|
|
486
|
+
return;
|
|
432
487
|
}
|
|
488
|
+
commitBtn.textContent = result === 'saved' ? 'Saved!' : 'Downloaded!';
|
|
433
489
|
commitBtn.classList.add('success');
|
|
434
490
|
setTimeout(() => {
|
|
435
|
-
commitBtn.textContent =
|
|
491
|
+
commitBtn.textContent = 'Commit Changes';
|
|
436
492
|
commitBtn.classList.remove('success');
|
|
437
493
|
commitBtn.disabled = false;
|
|
438
494
|
}, 2000);
|
|
439
495
|
} catch (e) {
|
|
440
496
|
commitBtn.textContent = 'Error: ' + e.message;
|
|
441
497
|
commitBtn.disabled = false;
|
|
442
|
-
setTimeout(() => { commitBtn.textContent =
|
|
498
|
+
setTimeout(() => { commitBtn.textContent = 'Commit Changes'; }, 3000);
|
|
443
499
|
}
|
|
444
500
|
};
|
|
445
501
|
panel.appendChild(commitBtn);
|
package/package.json
CHANGED
package/skill.md
CHANGED
|
@@ -89,13 +89,7 @@ Follow the generator's patterns for component structure, file naming, shader ini
|
|
|
89
89
|
## Step 6 — Inform About Fine-Tuner
|
|
90
90
|
|
|
91
91
|
Tell the user:
|
|
92
|
-
> Your BlueDither theme has been generated.
|
|
93
|
-
>
|
|
94
|
-
> ```
|
|
95
|
-
> npm run tune
|
|
96
|
-
> ```
|
|
97
|
-
>
|
|
98
|
-
> Open http://localhost:3333 to see your theme with a floating customization panel.
|
|
92
|
+
> Your BlueDither theme has been generated with the tuner panel built in. Adjust colors, fonts, spacing, and shader parameters live, then click "Commit Changes" to save.
|
|
99
93
|
|
|
100
94
|
## CSS Custom Property Computation
|
|
101
95
|
|
|
@@ -115,7 +109,7 @@ Border radius converts directly: `px / 16` rem (no clamp).
|
|
|
115
109
|
2. **Only content slots are editable** — never change colors, typography, spacing, shader, or opacity tokens during generation.
|
|
116
110
|
3. **Two sections only** — navbar + hero. No footers, sidebars, additional sections, icons, or images.
|
|
117
111
|
4. **Framework-native output** — use the framework's idioms (JSX for React, SFCs for Vue, etc.), never output raw HTML for a framework project.
|
|
118
|
-
5. **Shader is required** — the WebGL dithering shader must be initialized in every greenfield and hero-partial output.
|
|
112
|
+
5. **Shader is required** — the WebGL dithering shader must be initialized in every greenfield and hero-partial output. **Always assign the mount to `window.__BD_SHADER__`** so the tuner can update shader params live.
|
|
119
113
|
6. **CSS namespace** — all custom properties use `--bd-*` prefix. All classes use `bd-*` prefix.
|
|
120
114
|
7. **Headline is 2 lines max**, ALL CAPS, 4-6 words.
|
|
121
115
|
8. **Exactly 4 nav items.**
|
|
@@ -41,7 +41,7 @@ Navbar component with logo, nav items, and CTA.
|
|
|
41
41
|
### `BlueDitherHero.jsx`
|
|
42
42
|
Hero section with shader and text content.
|
|
43
43
|
|
|
44
|
-
- **Shader initialization**: Use `useEffect` + `useRef` for the shader parent div:
|
|
44
|
+
- **Shader initialization**: Use `useEffect` + `useRef` for the shader parent div. **CRITICAL**: Assign the mount to `window.__BD_SHADER__` so the tuner can update shader params live:
|
|
45
45
|
```jsx
|
|
46
46
|
import { useEffect, useRef } from 'react';
|
|
47
47
|
|
|
@@ -51,7 +51,8 @@ useEffect(() => {
|
|
|
51
51
|
// Dynamic import to avoid SSR issues
|
|
52
52
|
import('./paper-shaders-bundle.js').then(({ ShaderMount, ditheringFragmentShader, getShaderColorFromString }) => {
|
|
53
53
|
const mount = new ShaderMount(shaderRef.current, ditheringFragmentShader, uniforms, opts, speed, 0, 2);
|
|
54
|
-
|
|
54
|
+
window.__BD_SHADER__ = mount;
|
|
55
|
+
return () => { mount.dispose(); window.__BD_SHADER__ = null; };
|
|
55
56
|
});
|
|
56
57
|
}, []);
|
|
57
58
|
```
|
|
@@ -62,10 +62,12 @@ Hero section with shader initialization using `onMount`:
|
|
|
62
62
|
onMount(async () => {
|
|
63
63
|
const { ShaderMount, ditheringFragmentShader, getShaderColorFromString } = await import('./paper-shaders-bundle.js');
|
|
64
64
|
shaderMount = new ShaderMount(shaderParent, ditheringFragmentShader, uniforms, opts, speed, 0, 2);
|
|
65
|
+
window.__BD_SHADER__ = shaderMount;
|
|
65
66
|
});
|
|
66
67
|
|
|
67
68
|
onDestroy(() => {
|
|
68
69
|
shaderMount?.dispose();
|
|
70
|
+
window.__BD_SHADER__ = null;
|
|
69
71
|
});
|
|
70
72
|
</script>
|
|
71
73
|
|
|
@@ -32,9 +32,13 @@ URL-encode the font family name.
|
|
|
32
32
|
|
|
33
33
|
Copy `theme/shaders/bluedither-shader.js` and `theme/shaders/paper-shaders-bundle.js` to the target project.
|
|
34
34
|
|
|
35
|
-
Import and initialize inline:
|
|
35
|
+
Import and initialize inline. **CRITICAL**: Assign to `window.__BD_SHADER__` so the tuner can update shader params live:
|
|
36
36
|
```js
|
|
37
37
|
import { ShaderMount, ditheringFragmentShader, getShaderColorFromString } from './paper-shaders-bundle.js';
|
|
38
|
+
|
|
39
|
+
// After creating the mount:
|
|
40
|
+
const mount = new ShaderMount(parent, ditheringFragmentShader, uniforms, opts, speed, 0, 2);
|
|
41
|
+
window.__BD_SHADER__ = mount;
|
|
38
42
|
```
|
|
39
43
|
|
|
40
44
|
Map shader tokens to uniforms as shown in `template/index.html`'s existing `<script type="module">` block.
|
package/theme/generators/vue.md
CHANGED
|
@@ -66,10 +66,12 @@ let shaderMount = null;
|
|
|
66
66
|
onMounted(async () => {
|
|
67
67
|
const { ShaderMount, ditheringFragmentShader, getShaderColorFromString } = await import('./paper-shaders-bundle.js');
|
|
68
68
|
shaderMount = new ShaderMount(shaderParent.value, ditheringFragmentShader, uniforms, opts, speed, 0, 2);
|
|
69
|
+
window.__BD_SHADER__ = shaderMount;
|
|
69
70
|
});
|
|
70
71
|
|
|
71
72
|
onUnmounted(() => {
|
|
72
73
|
shaderMount?.dispose();
|
|
74
|
+
window.__BD_SHADER__ = null;
|
|
73
75
|
});
|
|
74
76
|
</script>
|
|
75
77
|
|