bluedither 1.0.9 → 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.
- package/cli/commands/install.js +14 -3
- package/cli/commands/tune.js +96 -41
- package/dist/bluedither-tuner.js +8 -8
- package/fine-tuner/inject.js +83 -0
- package/fine-tuner/server.js +20 -2
- package/fine-tuner/tuner.js +2 -2
- package/package.json +1 -1
package/cli/commands/install.js
CHANGED
|
@@ -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
|
-
|
|
137
|
-
|
|
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
|
|
package/cli/commands/tune.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
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
|
|
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
|
+
}
|
package/dist/bluedither-tuner.js
CHANGED
|
@@ -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__)
|
|
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">×</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=
|
|
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=
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
+
})();
|
package/fine-tuner/server.js
CHANGED
|
@@ -276,8 +276,26 @@ const server = createServer((req, res) => {
|
|
|
276
276
|
}
|
|
277
277
|
});
|
|
278
278
|
|
|
279
|
-
server.
|
|
280
|
-
|
|
279
|
+
server.on('error', (err) => {
|
|
280
|
+
if (err.code === 'EADDRINUSE') {
|
|
281
|
+
const nextPort = server._bdPort + 1;
|
|
282
|
+
if (nextPort > PORT + 20) {
|
|
283
|
+
console.error(`\n ✗ Could not find an available port (tried ${PORT}–${nextPort - 1}).\n`);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
server._bdPort = nextPort;
|
|
287
|
+
server.listen(nextPort);
|
|
288
|
+
} else {
|
|
289
|
+
throw err;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
server.on('listening', () => {
|
|
294
|
+
const actualPort = server.address().port;
|
|
295
|
+
console.log(`\n BlueDither Fine-Tuner running at http://localhost:${actualPort}\n`);
|
|
281
296
|
console.log(` Tokens: ${TOKENS_PATH}`);
|
|
282
297
|
console.log(` Edit in browser → hit Commit → tokens.json updated on disk.\n`);
|
|
283
298
|
});
|
|
299
|
+
|
|
300
|
+
server._bdPort = PORT;
|
|
301
|
+
server.listen(PORT);
|
package/fine-tuner/tuner.js
CHANGED
|
@@ -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
|
|
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 = [
|