bluedither 1.0.10 → 1.0.11

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.
@@ -94,6 +94,15 @@ export default async function install(args) {
94
94
  if (existsSync(tunerBundlePath)) {
95
95
  filesToCopy.push(['dist/bluedither-tuner.js', 'bluedither-tuner.js']);
96
96
  }
97
+ // Copy tuner CSS and injectable loader
98
+ const tunerCSSPath = resolve(BLUEDITHER_ROOT, 'fine-tuner', 'tuner.css');
99
+ if (existsSync(tunerCSSPath)) {
100
+ filesToCopy.push(['fine-tuner/tuner.css', 'bluedither-tuner.css']);
101
+ }
102
+ const injectPath = resolve(BLUEDITHER_ROOT, 'fine-tuner', 'inject.js');
103
+ if (existsSync(injectPath)) {
104
+ filesToCopy.push(['fine-tuner/inject.js', 'bluedither-tuner-inject.js']);
105
+ }
97
106
 
98
107
  let copied = 0;
99
108
  for (const [src, dest] of filesToCopy) {
@@ -132,9 +141,11 @@ $ARGUMENTS
132
141
  To apply the theme, use Claude Code:
133
142
  /apply-theme [description of your site]
134
143
 
135
- To customize visually:
136
- Include <script src="bluedither/bluedither-tuner.js"></script> in your page
137
- Or run: npx bluedither tune
144
+ To customize visually, add this to your HTML:
145
+ <script src="./bluedither/bluedither-tuner-inject.js"></script>
146
+
147
+ For auto-saving, run in a separate terminal:
148
+ npx bluedither tune --save-only
138
149
  `);
139
150
  }
140
151
 
@@ -1,16 +1,17 @@
1
1
  /**
2
- * bluedither tune [--port N]
2
+ * bluedither tune [--port N] [--save-only]
3
3
  *
4
- * Launches the fine-tuner dev server.
4
+ * Launches the fine-tuner dev server or save-only sidecar.
5
5
  * Finds the nearest bluedither theme directory — either:
6
6
  * - Source repo layout: ./theme/tokens.json + ./fine-tuner/server.js
7
7
  * - Installed layout: ./bluedither/tokens.json (from `npx bluedither install`)
8
8
  */
9
9
 
10
- import { existsSync } from 'fs';
10
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
11
11
  import { resolve, dirname } from 'path';
12
12
  import { fileURLToPath } from 'url';
13
- import { spawn } from 'child_process';
13
+ import { spawn, createServer as createHttpServer } from 'child_process';
14
+ import { createServer } from 'http';
14
15
 
15
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
16
17
  const PKG_ROOT = resolve(__dirname, '..', '..');
@@ -18,50 +19,21 @@ const PKG_ROOT = resolve(__dirname, '..', '..');
18
19
  export default async function tune(args) {
19
20
  if (args.includes('--help')) {
20
21
  console.log(`
21
- bluedither tune [--port N]
22
+ bluedither tune [--port N] [--save-only]
22
23
 
23
24
  Launches the fine-tuner dev server for real-time token editing.
24
25
  Searches for a BlueDither theme in the current directory.
25
26
 
26
27
  Options:
27
- --port N Port number (default: 3333)
28
+ --port N Port number (default: 3333)
29
+ --save-only Run only the save sidecar (port 3344) for use with
30
+ the injectable tuner in your own dev server.
28
31
  `);
29
32
  return;
30
33
  }
31
34
 
32
35
  const cwd = process.cwd();
33
-
34
- // Determine where the theme files live
35
- let themeDir = null;
36
- let tokensPath = null;
37
-
38
- // Case 1: Source repo layout (theme/ at cwd root)
39
- if (existsSync(resolve(cwd, 'theme', 'tokens.json'))) {
40
- themeDir = resolve(cwd, 'theme');
41
- tokensPath = resolve(themeDir, 'tokens.json');
42
- }
43
- // Case 2: Installed layout (bluedither/ subfolder from `npx bluedither install`)
44
- else if (existsSync(resolve(cwd, 'bluedither', 'tokens.json'))) {
45
- themeDir = resolve(cwd, 'bluedither');
46
- tokensPath = resolve(themeDir, 'tokens.json');
47
- }
48
- // Case 3: Search upward
49
- else {
50
- let dir = cwd;
51
- while (dir !== dirname(dir)) {
52
- if (existsSync(resolve(dir, 'theme', 'tokens.json'))) {
53
- themeDir = resolve(dir, 'theme');
54
- tokensPath = resolve(themeDir, 'tokens.json');
55
- break;
56
- }
57
- if (existsSync(resolve(dir, 'bluedither', 'tokens.json'))) {
58
- themeDir = resolve(dir, 'bluedither');
59
- tokensPath = resolve(themeDir, 'tokens.json');
60
- break;
61
- }
62
- dir = dirname(dir);
63
- }
64
- }
36
+ const themeDir = findThemeDir(cwd);
65
37
 
66
38
  if (!themeDir) {
67
39
  console.error('Could not find a BlueDither theme directory.');
@@ -69,15 +41,98 @@ export default async function tune(args) {
69
41
  process.exit(1);
70
42
  }
71
43
 
72
- // The fine-tuner server lives in the npm package
44
+ // Save-only sidecar mode
45
+ if (args.includes('--save-only')) {
46
+ return startSidecar(themeDir);
47
+ }
48
+
49
+ // Full fine-tuner server
73
50
  const serverPath = resolve(PKG_ROOT, 'fine-tuner', 'server.js');
74
51
  if (!existsSync(serverPath)) {
75
52
  console.error('Fine-tuner server not found at: ' + serverPath);
76
53
  process.exit(1);
77
54
  }
78
55
 
79
- // Pass the theme directory to the server via env var
80
56
  const env = { ...process.env, BD_THEME_DIR: themeDir };
81
- const child = spawn('node', [serverPath, ...args], { cwd: dirname(themeDir), env, stdio: 'inherit' });
57
+ const filteredArgs = args.filter(a => a !== '--save-only');
58
+ const child = spawn('node', [serverPath, ...filteredArgs], { cwd: dirname(themeDir), env, stdio: 'inherit' });
82
59
  child.on('exit', (code) => process.exit(code || 0));
83
60
  }
61
+
62
+ function findThemeDir(cwd) {
63
+ // Case 1: Source repo layout
64
+ if (existsSync(resolve(cwd, 'theme', 'tokens.json'))) {
65
+ return resolve(cwd, 'theme');
66
+ }
67
+ // Case 2: Installed layout
68
+ if (existsSync(resolve(cwd, 'bluedither', 'tokens.json'))) {
69
+ return resolve(cwd, 'bluedither');
70
+ }
71
+ // Case 3: Search upward
72
+ let dir = cwd;
73
+ while (dir !== dirname(dir)) {
74
+ if (existsSync(resolve(dir, 'theme', 'tokens.json'))) return resolve(dir, 'theme');
75
+ if (existsSync(resolve(dir, 'bluedither', 'tokens.json'))) return resolve(dir, 'bluedither');
76
+ dir = dirname(dir);
77
+ }
78
+ return null;
79
+ }
80
+
81
+ function startSidecar(themeDir) {
82
+ const SIDECAR_PORT = 3344;
83
+ const tokensPath = resolve(themeDir, 'tokens.json');
84
+
85
+ const server = createServer((req, res) => {
86
+ // CORS headers for cross-origin requests from any dev server
87
+ res.setHeader('Access-Control-Allow-Origin', '*');
88
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
89
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
90
+
91
+ if (req.method === 'OPTIONS') {
92
+ res.writeHead(204);
93
+ res.end();
94
+ return;
95
+ }
96
+
97
+ if (req.method === 'GET' && req.url === '/ping') {
98
+ res.writeHead(200, { 'Content-Type': 'application/json' });
99
+ res.end('{"ok":true}');
100
+ return;
101
+ }
102
+
103
+ if (req.method === 'POST' && req.url === '/commit') {
104
+ let body = '';
105
+ req.on('data', chunk => { body += chunk; });
106
+ req.on('end', () => {
107
+ try {
108
+ const data = JSON.parse(body);
109
+ writeFileSync(tokensPath, JSON.stringify(data, null, 2));
110
+ res.writeHead(200, { 'Content-Type': 'application/json' });
111
+ res.end('{"ok":true}');
112
+ console.log(' ✓ Tokens saved');
113
+ } catch (e) {
114
+ res.writeHead(400);
115
+ res.end(`{"error":"${e.message}"}`);
116
+ }
117
+ });
118
+ return;
119
+ }
120
+
121
+ res.writeHead(404);
122
+ res.end();
123
+ });
124
+
125
+ server.on('error', (err) => {
126
+ if (err.code === 'EADDRINUSE') {
127
+ console.error(`\n ✗ Port ${SIDECAR_PORT} is in use. The sidecar may already be running.\n`);
128
+ process.exit(1);
129
+ }
130
+ throw err;
131
+ });
132
+
133
+ server.listen(SIDECAR_PORT, () => {
134
+ console.log(`\n BlueDither save sidecar running on port ${SIDECAR_PORT}`);
135
+ console.log(` Tokens: ${tokensPath}`);
136
+ console.log(` The tuner in your browser will auto-save changes here.\n`);
137
+ });
138
+ }
@@ -1,4 +1,4 @@
1
- (()=>{var u=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||"{}")),N=!!(window.__BD_TOKENS__&&window.__BD_DEFAULTS__),$=["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 a=t.split("."),l=n;for(let s=0;s<a.length-1;s++)l=l[a[s]];l[a[a.length-1]]=e}function _(n,t){return t.split(".").reduce((e,a)=>e?.[a],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 j(n){let t=u.layout.designWidth,e=n/16,a=n/t*100;return`clamp(${(e*.55).toFixed(4)}rem, ${a.toFixed(4)}vw, ${e.toFixed(4)}rem)`}function P(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=`
1
+ (()=>{var u=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||"{}")),N=!!(window.__BD_TOKENS__&&window.__BD_DEFAULTS__)||!!window.__BD_TUNER_SERVER_MODE__,P=["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([...P,...H])].sort();function v(n,t,e){let a=t.split("."),l=n;for(let s=0;s<a.length-1;s++)l=l[a[s]];l[a[a.length-1]]=e}function E(n,t){return t.split(".").reduce((e,a)=>e?.[a],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 j(n){let t=u.layout.designWidth,e=n/16,a=n/t*100;return`clamp(${(e*.55).toFixed(4)}rem, ${a.toFixed(4)}vw, ${e.toFixed(4)}rem)`}function D(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,11 +6,11 @@
6
6
  <button id="bd-tuner-close" title="Close">&times;</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(u,structuredClone(O)),window.__BD_SERVER_MODE__?fetch("/commit",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(u,null,2)}).then(()=>location.reload()):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,a,l){let s=_(u,e)||"#000000",r=document.createElement("div");r.className="bd-tuner-row",r.innerHTML=`
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(u,structuredClone(O)),window.__BD_SERVER_MODE__?fetch("/commit",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(u,null,2)}).then(()=>location.reload()):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,a,l){let s=E(u,e)||"#000000",r=document.createElement("div");r.className="bd-tuner-row",r.innerHTML=`
10
10
  <span class="bd-tuner-label">${t}</span>
11
11
  <span class="bd-tuner-input"><input type="color" value="${s.substring(0,7)}"></span>
12
12
  <span class="bd-tuner-value">${s.substring(0,7)}</span>
13
- `;let i=r.querySelector("input"),d=r.querySelector(".bd-tuner-value");return i.addEventListener("input",c=>{let m=c.target.value;v(u,e,m),d.textContent=m,a&&k(a,m),l&&l(m)}),n.appendChild(r),{setValue(c){v(u,e,c),i.value=c.substring(0,7),d.textContent=c.substring(0,7),a&&k(a,c)}}}function g(n,t,e,a,l,s,r){let i=_(u,e),d=document.createElement("div");d.className="bd-tuner-row bd-tuner-row-stacked",d.innerHTML=`
13
+ `;let i=r.querySelector("input"),d=r.querySelector(".bd-tuner-value");return i.addEventListener("input",c=>{let m=c.target.value;v(u,e,m),d.textContent=m,a&&k(a,m),l&&l(m)}),n.appendChild(r),{setValue(c){v(u,e,c),i.value=c.substring(0,7),d.textContent=c.substring(0,7),a&&k(a,c)}}}function g(n,t,e,a,l,s,r){let i=E(u,e),d=document.createElement("div");d.className="bd-tuner-row bd-tuner-row-stacked",d.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">
@@ -19,24 +19,24 @@
19
19
  </div>
20
20
  </div>
21
21
  <input type="range" class="bd-tuner-slider" min="${a}" max="${l}" step="${s}" value="${i}">
22
- `;let c=d.querySelector(".bd-tuner-px-num"),m=d.querySelector(".bd-tuner-slider");function b(o){if(o=Math.max(a,Math.min(l,o)),v(u,e,o),c.value=o,m.value=o,r){let w=j(o);(Array.isArray(r)?r:[r]).forEach(f=>k(f,w))}}c.addEventListener("input",o=>b(parseFloat(o.target.value)||0)),m.addEventListener("input",o=>b(parseFloat(o.target.value))),c.addEventListener("keydown",o=>{if(o.key==="ArrowUp"||o.key==="ArrowDown"){o.preventDefault();let w=o.shiftKey?10:1,f=(o.key==="ArrowUp"?s:-s)*w;b(parseFloat(c.value)+f)}}),n.appendChild(d)}function F(n,t,e,a,l,s,r){let i=_(u,e),d=document.createElement("div");d.className="bd-tuner-row bd-tuner-row-stacked",d.innerHTML=`
22
+ `;let c=d.querySelector(".bd-tuner-px-num"),m=d.querySelector(".bd-tuner-slider");function b(o){if(o=Math.max(a,Math.min(l,o)),v(u,e,o),c.value=o,m.value=o,r){let w=j(o);(Array.isArray(r)?r:[r]).forEach(f=>k(f,w))}}c.addEventListener("input",o=>b(parseFloat(o.target.value)||0)),m.addEventListener("input",o=>b(parseFloat(o.target.value))),c.addEventListener("keydown",o=>{if(o.key==="ArrowUp"||o.key==="ArrowDown"){o.preventDefault();let w=o.shiftKey?10:1,f=(o.key==="ArrowUp"?s:-s)*w;b(parseFloat(c.value)+f)}}),n.appendChild(d)}function F(n,t,e,a,l,s,r){let i=E(u,e),d=document.createElement("div");d.className="bd-tuner-row bd-tuner-row-stacked",d.innerHTML=`
23
23
  <div class="bd-tuner-row-top">
24
24
  <span class="bd-tuner-label">${t}</span>
25
25
  <span class="bd-tuner-value">${i}</span>
26
26
  </div>
27
27
  <input type="range" class="bd-tuner-slider" min="${a}" max="${l}" step="${s}" value="${i}">
28
- `;let c=d.querySelector(".bd-tuner-slider"),m=d.querySelector(".bd-tuner-value");c.addEventListener("input",b=>{let o=parseFloat(b.target.value);v(u,e,o),m.textContent=o,r&&r(o)}),n.appendChild(d)}function D(n,t,e,a){let l=_(u,e),s=document.createElement("div");s.className="bd-tuner-row",s.innerHTML=`
28
+ `;let c=d.querySelector(".bd-tuner-slider"),m=d.querySelector(".bd-tuner-value");c.addEventListener("input",b=>{let o=parseFloat(b.target.value);v(u,e,o),m.textContent=o,r&&r(o)}),n.appendChild(d)}function $(n,t,e,a){let l=E(u,e),s=document.createElement("div");s.className="bd-tuner-row",s.innerHTML=`
29
29
  <span class="bd-tuner-label">${t}</span>
30
30
  <span class="bd-tuner-input bd-tuner-font-input">
31
31
  <input type="text" class="bd-tuner-font-search" value="${l}" placeholder="Search fonts...">
32
32
  <div class="bd-tuner-font-dropdown"></div>
33
33
  </span>
34
- `;let r=s.querySelector(".bd-tuner-font-search"),i=s.querySelector(".bd-tuner-font-dropdown"),d=m=>$.some(b=>b.toLowerCase()===m.toLowerCase());function c(m=""){let b=A.filter(o=>o.toLowerCase().includes(m.toLowerCase())).slice(0,20);i.innerHTML=b.map(o=>{let w=d(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}${w}</div>`}).join(""),b.filter(o=>!d(o)).forEach(P),i.querySelectorAll(".bd-tuner-font-option").forEach(o=>{o.addEventListener("mousedown",w=>{w.preventDefault();let f=o.dataset.font;r.value=f,v(u,e,f),d(f)||P(f),k(a,`"${f}", system-ui, sans-serif`),i.classList.remove("open")})})}r.addEventListener("focus",()=>{c(r.value),i.classList.add("open")}),r.addEventListener("input",()=>{c(r.value),i.classList.add("open")}),r.addEventListener("blur",()=>{setTimeout(()=>i.classList.remove("open"),150)}),r.addEventListener("keydown",m=>{if(m.key==="Enter"){let b=r.value.trim();b&&(v(u,e,b),d(b)||P(b),k(a,`"${b}", system-ui, sans-serif`),i.classList.remove("open"),r.blur())}}),n.appendChild(s)}function B(n,t,e,a,l){let s=_(u,e),r=document.createElement("div");r.className="bd-tuner-row bd-tuner-row-stacked",r.innerHTML=`
34
+ `;let r=s.querySelector(".bd-tuner-font-search"),i=s.querySelector(".bd-tuner-font-dropdown"),d=m=>P.some(b=>b.toLowerCase()===m.toLowerCase());function c(m=""){let b=A.filter(o=>o.toLowerCase().includes(m.toLowerCase())).slice(0,20);i.innerHTML=b.map(o=>{let w=d(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}${w}</div>`}).join(""),b.filter(o=>!d(o)).forEach(D),i.querySelectorAll(".bd-tuner-font-option").forEach(o=>{o.addEventListener("mousedown",w=>{w.preventDefault();let f=o.dataset.font;r.value=f,v(u,e,f),d(f)||D(f),k(a,`"${f}", system-ui, sans-serif`),i.classList.remove("open")})})}r.addEventListener("focus",()=>{c(r.value),i.classList.add("open")}),r.addEventListener("input",()=>{c(r.value),i.classList.add("open")}),r.addEventListener("blur",()=>{setTimeout(()=>i.classList.remove("open"),150)}),r.addEventListener("keydown",m=>{if(m.key==="Enter"){let b=r.value.trim();b&&(v(u,e,b),d(b)||D(b),k(a,`"${b}", system-ui, sans-serif`),i.classList.remove("open"),r.blur())}}),n.appendChild(s)}function R(n,t,e,a,l){let s=E(u,e),r=document.createElement("div");r.className="bd-tuner-row bd-tuner-row-stacked",r.innerHTML=`
35
35
  <span class="bd-tuner-label">${t}</span>
36
36
  <div class="bd-tuner-segmented">
37
37
  ${a.map(i=>`<button class="bd-tuner-seg-btn${i===s?" active":""}" data-val="${i}">${i}</button>`).join("")}
38
38
  </div>
39
- `,r.querySelectorAll(".bd-tuner-seg-btn").forEach(i=>{i.addEventListener("click",()=>{r.querySelectorAll(".bd-tuner-seg-btn").forEach(c=>c.classList.remove("active")),i.classList.add("active");let d=i.dataset.val;v(u,e,d),l&&l(d)})}),n.appendChild(r)}var E=T("Colors");C(E,"Background","colors.background","--bd-color-background");var z;C(E,"Primary","colors.primary","--bd-color-primary",n=>{z.setValue(n),S("colorFront",n)});C(E,"Text","colors.text","--bd-color-text");C(E,"CTA Background","colors.ctaBackground","--bd-color-cta-bg");C(E,"CTA Text","colors.ctaText","--bd-color-cta-text");z=C(E,"Shader Front","colors.shaderFront",null,n=>S("colorFront",n));var y=T("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-subheadline-size");g(y,"Sub LH","typography.subHeadline.lineHeightPx",12,80,1,"--bd-subheadline-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");var h=T("Spacing");g(h,"Header Pad X","spacing.headerPaddingX",0,80,1,"--bd-header-px");g(h,"Header Pad Y","spacing.headerPaddingY",0,60,1,"--bd-header-py");g(h,"Hero Pad Top","spacing.heroPaddingTop",0,120,1,"--bd-hero-pt");g(h,"Hero Pad Bottom","spacing.heroPaddingBottom",0,120,1,"--bd-hero-pb");g(h,"Hero Pad X","spacing.heroPaddingX",0,120,1,"--bd-hero-px");g(h,"Nav Gap","spacing.navGap",0,100,1,"--bd-nav-gap");g(h,"CTA Pad X","spacing.ctaPaddingX",0,60,1,"--bd-cta-px");g(h,"CTA Pad Y","spacing.ctaPaddingY",0,30,1,"--bd-cta-py");g(h,"CTA Radius","spacing.ctaBorderRadius",0,32,1,"--bd-cta-radius");var L=T("Shader");B(L,"Shape","shader.shape",["warp","simplex","dots","wave","ripple","swirl","sphere"],n=>S("shape",n));B(L,"Dither Type","shader.type",["random","2x2","4x4","8x8"],n=>S("type",n));F(L,"Speed","shader.speed",0,2,.01,n=>S("speed",n));F(L,"Scale","shader.scale",.1,5,.01,n=>S("scale",n));F(L,"Dither Size","shader.size",.5,10,.1,n=>S("size",n));var q=T("Opacity");F(q,"Nav Links","opacity.navLinks",0,1,.01,n=>k("--bd-nav-opacity",n));var p=document.createElement("button");p.id="bd-tuner-commit";p.textContent="Commit Changes";var I=N?"Commit Changes":"Export Tokens";p.textContent=I;p.onclick=async()=>{p.textContent="Exporting...",p.disabled=!0;try{if(window.__BD_SERVER_MODE__){let t=await(await fetch("/commit",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(u,null,2)})).json();if(!t.ok)throw new Error(t.error);p.textContent="Committed!"}else{let n=new Blob([JSON.stringify(u,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),p.textContent="Downloaded!"}p.classList.add("success"),setTimeout(()=>{p.textContent=window.__BD_SERVER_MODE__?"Commit Changes":"Export Tokens",p.classList.remove("success"),p.disabled=!1},2e3)}catch(n){p.textContent="Error: "+n.message,p.disabled=!1,setTimeout(()=>{p.textContent=window.__BD_SERVER_MODE__?"Commit Changes":"Export Tokens"},3e3)}};window.__BD_SERVER_MODE__||(p.textContent="Export Tokens");x.appendChild(p);var R=document.createElement("style");R.textContent=`/* =============================================
39
+ `,r.querySelectorAll(".bd-tuner-seg-btn").forEach(i=>{i.addEventListener("click",()=>{r.querySelectorAll(".bd-tuner-seg-btn").forEach(c=>c.classList.remove("active")),i.classList.add("active");let d=i.dataset.val;v(u,e,d),l&&l(d)})}),n.appendChild(r)}var _=T("Colors");C(_,"Background","colors.background","--bd-color-background");var B;C(_,"Primary","colors.primary","--bd-color-primary",n=>{B.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");B=C(_,"Shader Front","colors.shaderFront",null,n=>S("colorFront",n));var y=T("Typography");$(y,"Primary Font","typography.primaryFont","--bd-font-primary");$(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-subheadline-size");g(y,"Sub LH","typography.subHeadline.lineHeightPx",12,80,1,"--bd-subheadline-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");var h=T("Spacing");g(h,"Header Pad X","spacing.headerPaddingX",0,80,1,"--bd-header-px");g(h,"Header Pad Y","spacing.headerPaddingY",0,60,1,"--bd-header-py");g(h,"Hero Pad Top","spacing.heroPaddingTop",0,120,1,"--bd-hero-pt");g(h,"Hero Pad Bottom","spacing.heroPaddingBottom",0,120,1,"--bd-hero-pb");g(h,"Hero Pad X","spacing.heroPaddingX",0,120,1,"--bd-hero-px");g(h,"Nav Gap","spacing.navGap",0,100,1,"--bd-nav-gap");g(h,"CTA Pad X","spacing.ctaPaddingX",0,60,1,"--bd-cta-px");g(h,"CTA Pad Y","spacing.ctaPaddingY",0,30,1,"--bd-cta-py");g(h,"CTA Radius","spacing.ctaBorderRadius",0,32,1,"--bd-cta-radius");var L=T("Shader");R(L,"Shape","shader.shape",["warp","simplex","dots","wave","ripple","swirl","sphere"],n=>S("shape",n));R(L,"Dither Type","shader.type",["random","2x2","4x4","8x8"],n=>S("type",n));F(L,"Speed","shader.speed",0,2,.01,n=>S("speed",n));F(L,"Scale","shader.scale",.1,5,.01,n=>S("scale",n));F(L,"Dither Size","shader.size",.5,10,.1,n=>S("size",n));var q=T("Opacity");F(q,"Nav Links","opacity.navLinks",0,1,.01,n=>k("--bd-nav-opacity",n));var p=document.createElement("button");p.id="bd-tuner-commit";p.textContent="Commit Changes";var I=N?"Commit Changes":"Export Tokens";p.textContent=I;p.onclick=async()=>{p.textContent="Exporting...",p.disabled=!0;try{if(window.__BD_SERVER_MODE__){let t=await(await fetch("/commit",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(u,null,2)})).json();if(!t.ok)throw new Error(t.error);p.textContent="Committed!"}else{let n=new Blob([JSON.stringify(u,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),p.textContent="Downloaded!"}p.classList.add("success"),setTimeout(()=>{p.textContent=window.__BD_SERVER_MODE__?"Commit Changes":"Export Tokens",p.classList.remove("success"),p.disabled=!1},2e3)}catch(n){p.textContent="Error: "+n.message,p.disabled=!1,setTimeout(()=>{p.textContent=window.__BD_SERVER_MODE__?"Commit Changes":"Export Tokens"},3e3)}};window.__BD_SERVER_MODE__||(p.textContent="Export Tokens");x.appendChild(p);var z=document.createElement("style");z.textContent=`/* =============================================
40
40
  BlueDither Fine-Tuner \u2014 Overlay Panel v2
41
41
  ============================================= */
42
42
 
@@ -398,4 +398,4 @@
398
398
  background: rgba(255, 255, 255, 0.1);
399
399
  border-radius: 2px;
400
400
  }
401
- `;document.head.appendChild(R);})();
401
+ `;document.head.appendChild(z);})();
@@ -0,0 +1,83 @@
1
+ /**
2
+ * BlueDither Tuner — Injectable Loader
3
+ *
4
+ * Drop this script into ANY page to get the tuner overlay.
5
+ * Reads tokens from bluedither/tokens.json, updates CSS vars live.
6
+ * Saves via sidecar (if running) or downloads the file.
7
+ *
8
+ * Usage:
9
+ * <script src="./bluedither/bluedither-tuner-inject.js"></script>
10
+ *
11
+ * The sidecar save server:
12
+ * npx bluedither tune --save-only
13
+ */
14
+
15
+ (async function () {
16
+ // Find the base path to bluedither/ directory
17
+ const scriptEl = document.currentScript;
18
+ const scriptSrc = scriptEl?.src || '';
19
+ const basePath = scriptSrc.replace(/\/[^/]+$/, '');
20
+
21
+ // Load tokens
22
+ let tokens, defaults;
23
+ try {
24
+ const tokensRes = await fetch(`${basePath}/tokens.json`);
25
+ tokens = await tokensRes.json();
26
+ } catch {
27
+ console.warn('[BlueDither Tuner] Could not load tokens.json');
28
+ return;
29
+ }
30
+ try {
31
+ const defaultsRes = await fetch(`${basePath}/tokens.defaults.json`);
32
+ defaults = await defaultsRes.json();
33
+ } catch {
34
+ defaults = structuredClone(tokens);
35
+ }
36
+
37
+ // Inject into globals for the tuner
38
+ window.__BD_TOKENS__ = tokens;
39
+ window.__BD_DEFAULTS__ = defaults;
40
+
41
+ // Override server mode — use sidecar instead of /commit
42
+ const SIDECAR_PORT = 3344;
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
+ }
65
+
66
+ // Load tuner CSS
67
+ try {
68
+ const cssRes = await fetch(`${basePath}/bluedither-tuner.css`);
69
+ if (cssRes.ok) {
70
+ const css = await cssRes.text();
71
+ const style = document.createElement('style');
72
+ style.textContent = css;
73
+ document.head.appendChild(style);
74
+ }
75
+ } catch {
76
+ // CSS will be inlined in the bundled version
77
+ }
78
+
79
+ // Load and execute tuner script
80
+ const tunerScript = document.createElement('script');
81
+ tunerScript.src = `${basePath}/bluedither-tuner.js`;
82
+ document.body.appendChild(tunerScript);
83
+ })();
@@ -18,8 +18,8 @@ const defaults = structuredClone(
18
18
  JSON.parse(document.querySelector('script[type="application/json"][data-bluedither-defaults]')?.textContent || '{}')
19
19
  );
20
20
 
21
- /** True when served by fine-tuner/server.js (has /commit endpoint) */
22
- const __serverMode = !!(window.__BD_TOKENS__ && window.__BD_DEFAULTS__);
21
+ /** True when served by fine-tuner/server.js or sidecar is available */
22
+ const __serverMode = !!(window.__BD_TOKENS__ && window.__BD_DEFAULTS__) || !!window.__BD_TUNER_SERVER_MODE__;
23
23
 
24
24
  // ── System fonts (always available, no loading needed) ──
25
25
  const SYSTEM_FONTS = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bluedither",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "A bold, dithered-shader hero theme for Claude Code — skill + fine-tuner",
5
5
  "type": "module",
6
6
  "bin": {