bluedither 1.0.0 → 1.0.2
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/bin.js +5 -1
- package/cli/commands/install.js +92 -2
- package/cli/commands/login.js +71 -0
- package/cli/commands/publish.js +109 -16
- package/cli/commands/search.js +64 -0
- package/cli/commands/tune.js +47 -27
- package/cli/lib/credentials.js +26 -0
- package/cli/lib/registry.js +58 -0
- package/fine-tuner/server.js +283 -0
- package/fine-tuner/tuner.css +362 -0
- package/fine-tuner/tuner.js +445 -0
- package/package.json +5 -2
- package/theme/tokens.defaults.json +5 -5
- package/theme/tokens.json +5 -5
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BlueDither Fine-Tuner Dev Server
|
|
3
|
+
*
|
|
4
|
+
* Serves the rendered theme with tuner overlay.
|
|
5
|
+
* Computes clamp() values from reference px values at render time.
|
|
6
|
+
* Handles "commit" to write tokens back to disk.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node fine-tuner/server.js [--port 3333]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
12
|
+
import { createServer } from 'http';
|
|
13
|
+
import { resolve, extname, dirname } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
// BD_THEME_DIR env var is set by `bluedither tune` for installed themes
|
|
18
|
+
const THEME_DIR = process.env.BD_THEME_DIR || resolve(__dirname, '..', 'theme');
|
|
19
|
+
const TOKENS_PATH = resolve(THEME_DIR, 'tokens.json');
|
|
20
|
+
const PORT = parseInt(process.argv.find((_, i, a) => a[i - 1] === '--port') || '3333', 10);
|
|
21
|
+
|
|
22
|
+
function loadTokens() {
|
|
23
|
+
return JSON.parse(readFileSync(TOKENS_PATH, 'utf-8'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveTokenPath(obj, path) {
|
|
27
|
+
return path.split('.').reduce((o, k) => (o && o[k] !== undefined ? o[k] : ''), obj);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Compute a clamp() value from a reference px size.
|
|
32
|
+
* Formula: clamp(minRem, vwValue, maxRem)
|
|
33
|
+
* - max = referencePx / 16 (rem at design width)
|
|
34
|
+
* - vw = referencePx / designWidth * 100
|
|
35
|
+
* - min = max * 0.55 (scales down to ~55% on small screens)
|
|
36
|
+
*/
|
|
37
|
+
function pxToClamp(px, designWidth) {
|
|
38
|
+
const maxRem = px / 16;
|
|
39
|
+
const vw = (px / designWidth) * 100;
|
|
40
|
+
const minRem = maxRem * 0.55;
|
|
41
|
+
return `clamp(${minRem.toFixed(4)}rem, ${vw.toFixed(4)}vw, ${maxRem.toFixed(4)}rem)`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build computed clamp values from tokens.
|
|
46
|
+
*/
|
|
47
|
+
function buildClampValues(tokens) {
|
|
48
|
+
const dw = tokens.layout.designWidth;
|
|
49
|
+
const t = tokens.typography;
|
|
50
|
+
const s = tokens.spacing;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
headline: {
|
|
54
|
+
fontSize: pxToClamp(t.headline.referencePx, dw),
|
|
55
|
+
lineHeight: pxToClamp(t.headline.lineHeightPx, dw),
|
|
56
|
+
},
|
|
57
|
+
subHeadline: {
|
|
58
|
+
fontSize: pxToClamp(t.subHeadline.referencePx, dw),
|
|
59
|
+
lineHeight: pxToClamp(t.subHeadline.lineHeightPx, dw),
|
|
60
|
+
},
|
|
61
|
+
logo: {
|
|
62
|
+
fontSize: pxToClamp(t.logo.referencePx, dw),
|
|
63
|
+
lineHeight: pxToClamp(t.logo.lineHeightPx, dw),
|
|
64
|
+
},
|
|
65
|
+
navItem: {
|
|
66
|
+
fontSize: pxToClamp(t.navItem.referencePx, dw),
|
|
67
|
+
lineHeight: pxToClamp(t.navItem.lineHeightPx, dw),
|
|
68
|
+
},
|
|
69
|
+
ctaButton: {
|
|
70
|
+
fontSize: pxToClamp(t.ctaButton.referencePx, dw),
|
|
71
|
+
lineHeight: pxToClamp(t.ctaButton.lineHeightPx, dw),
|
|
72
|
+
},
|
|
73
|
+
spacing: {
|
|
74
|
+
headerPaddingX: pxToClamp(s.headerPaddingX, dw),
|
|
75
|
+
headerPaddingY: pxToClamp(s.headerPaddingY, dw),
|
|
76
|
+
heroPaddingTop: pxToClamp(s.heroPaddingTop, dw),
|
|
77
|
+
heroPaddingBottom: pxToClamp(s.heroPaddingBottom, dw),
|
|
78
|
+
heroPaddingX: pxToClamp(s.heroPaddingX, dw),
|
|
79
|
+
navGap: pxToClamp(s.navGap, dw),
|
|
80
|
+
ctaPaddingX: pxToClamp(s.ctaPaddingX, dw),
|
|
81
|
+
ctaPaddingY: pxToClamp(s.ctaPaddingY, dw),
|
|
82
|
+
ctaBorderRadius: `${(s.ctaBorderRadius / 16).toFixed(4)}rem`,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function renderFullPage(tokens) {
|
|
88
|
+
let template = readFileSync(resolve(THEME_DIR, 'template', 'index.html'), 'utf-8');
|
|
89
|
+
|
|
90
|
+
// Build clamp values
|
|
91
|
+
const clamp = buildClampValues(tokens);
|
|
92
|
+
const renderData = { ...tokens, clamp };
|
|
93
|
+
|
|
94
|
+
// Handle font URL encoding for Google Fonts links
|
|
95
|
+
template = template.replace(/\{\{typography\.primaryFont\.urlEncoded\}\}/g,
|
|
96
|
+
encodeURIComponent(tokens.typography.primaryFont));
|
|
97
|
+
template = template.replace(/\{\{typography\.secondaryFont\.urlEncoded\}\}/g,
|
|
98
|
+
encodeURIComponent(tokens.typography.secondaryFont));
|
|
99
|
+
|
|
100
|
+
// Handle {{#each}} blocks
|
|
101
|
+
template = template.replace(
|
|
102
|
+
/\{\{#each\s+([^}]+)\}\}([\s\S]*?)\{\{\/each\}\}/g,
|
|
103
|
+
(_, arrayPath, inner) => {
|
|
104
|
+
const arr = resolveTokenPath(renderData, arrayPath.trim());
|
|
105
|
+
if (!Array.isArray(arr)) return '';
|
|
106
|
+
return arr.map(item => inner.replace(/\{\{this\}\}/g, item)).join('\n');
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Replace remaining {{path}} tokens
|
|
111
|
+
template = template.replace(/\{\{([^#/][^}]*)\}\}/g, (_, path) => {
|
|
112
|
+
const val = resolveTokenPath(renderData, path.trim());
|
|
113
|
+
return val !== undefined && val !== '' ? val : '';
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Fix headline newlines
|
|
117
|
+
template = template.replace(
|
|
118
|
+
/(<div class="bd-headline">)([\s\S]*?)(<\/div>)/,
|
|
119
|
+
(_, open, content, close) => open + content.replace(/\n/g, '<br>') + close
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Inject shader, tokens, and tuner
|
|
123
|
+
const shaderJS = readFileSync(resolve(THEME_DIR, 'shaders', 'bluedither-shader.js'), 'utf-8');
|
|
124
|
+
const shaderBundle = readFileSync(resolve(THEME_DIR, 'shaders', 'paper-shaders-bundle.js'), 'utf-8');
|
|
125
|
+
const tunerCSS = readFileSync(resolve(__dirname, 'tuner.css'), 'utf-8');
|
|
126
|
+
const tunerJS = readFileSync(resolve(__dirname, 'tuner.js'), 'utf-8');
|
|
127
|
+
|
|
128
|
+
const injection = `
|
|
129
|
+
<script>
|
|
130
|
+
window.__BD_TOKENS__ = ${JSON.stringify(tokens, null, 2)};
|
|
131
|
+
window.__BD_DEFAULTS__ = ${JSON.stringify(loadDefaults(), null, 2)};
|
|
132
|
+
</script>
|
|
133
|
+
<script type="module">
|
|
134
|
+
// Paper shaders bundle (inlined)
|
|
135
|
+
${shaderBundle}
|
|
136
|
+
|
|
137
|
+
// BlueDither shader module (inlined, imports resolved above)
|
|
138
|
+
const shapeEnum = { simplex: 1, warp: 2, dots: 3, wave: 4, ripple: 5, swirl: 6, sphere: 7 };
|
|
139
|
+
const typeEnum = { random: 1, '2x2': 2, '4x4': 3, '8x8': 4 };
|
|
140
|
+
|
|
141
|
+
const parentEl = document.getElementById('bd-shader-parent');
|
|
142
|
+
const tokens = window.__BD_TOKENS__;
|
|
143
|
+
|
|
144
|
+
const uniforms = {
|
|
145
|
+
u_colorFront: getShaderColorFromString(tokens.colors.shaderFront),
|
|
146
|
+
u_colorBack: getShaderColorFromString(tokens.colors.shaderBack),
|
|
147
|
+
u_shape: shapeEnum[tokens.shader.shape] || 2,
|
|
148
|
+
u_type: typeEnum[tokens.shader.type] || 2,
|
|
149
|
+
u_pxSize: tokens.shader.size,
|
|
150
|
+
u_scale: tokens.shader.scale,
|
|
151
|
+
u_rotation: 0,
|
|
152
|
+
u_originX: 0.5,
|
|
153
|
+
u_originY: 0.5,
|
|
154
|
+
u_worldWidth: 0,
|
|
155
|
+
u_worldHeight: 0,
|
|
156
|
+
u_fit: 0,
|
|
157
|
+
u_offsetX: 0,
|
|
158
|
+
u_offsetY: 0,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const mount = new ShaderMount(
|
|
162
|
+
parentEl,
|
|
163
|
+
ditheringFragmentShader,
|
|
164
|
+
uniforms,
|
|
165
|
+
{ alpha: true, premultipliedAlpha: false },
|
|
166
|
+
tokens.shader.speed,
|
|
167
|
+
0,
|
|
168
|
+
2,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Expose for tuner
|
|
172
|
+
window.__BD_SHADER__ = {
|
|
173
|
+
updateParams(p) {
|
|
174
|
+
const u = {};
|
|
175
|
+
if (p.colorFront !== undefined) u.u_colorFront = getShaderColorFromString(p.colorFront);
|
|
176
|
+
if (p.colorBack !== undefined) u.u_colorBack = getShaderColorFromString(p.colorBack);
|
|
177
|
+
if (p.shape !== undefined) u.u_shape = shapeEnum[p.shape] || 2;
|
|
178
|
+
if (p.type !== undefined) u.u_type = typeEnum[p.type] || 2;
|
|
179
|
+
if (p.size !== undefined) u.u_pxSize = p.size;
|
|
180
|
+
if (p.scale !== undefined) u.u_scale = p.scale;
|
|
181
|
+
if (p.speed !== undefined) mount.setSpeed(p.speed);
|
|
182
|
+
if (Object.keys(u).length > 0) mount.setUniforms(u);
|
|
183
|
+
},
|
|
184
|
+
destroy() { mount.dispose(); }
|
|
185
|
+
};
|
|
186
|
+
</script>
|
|
187
|
+
<style>${tunerCSS}</style>
|
|
188
|
+
<script type="module">${tunerJS}</script>
|
|
189
|
+
`;
|
|
190
|
+
|
|
191
|
+
template = template.replace('</body>', injection + '\n</body>');
|
|
192
|
+
return template;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function loadDefaults() {
|
|
196
|
+
// Store the original shipped tokens as defaults for reset
|
|
197
|
+
const defaultsPath = resolve(THEME_DIR, 'tokens.defaults.json');
|
|
198
|
+
try {
|
|
199
|
+
return JSON.parse(readFileSync(defaultsPath, 'utf-8'));
|
|
200
|
+
} catch {
|
|
201
|
+
// If no defaults file, current tokens ARE the defaults
|
|
202
|
+
return loadTokens();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const MIME = {
|
|
207
|
+
'.html': 'text/html',
|
|
208
|
+
'.js': 'application/javascript',
|
|
209
|
+
'.css': 'text/css',
|
|
210
|
+
'.json': 'application/json',
|
|
211
|
+
'.png': 'image/png',
|
|
212
|
+
'.jpg': 'image/jpeg',
|
|
213
|
+
'.svg': 'image/svg+xml',
|
|
214
|
+
'.woff2': 'font/woff2',
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const server = createServer((req, res) => {
|
|
218
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
219
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
220
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
221
|
+
|
|
222
|
+
if (req.method === 'OPTIONS') {
|
|
223
|
+
res.writeHead(204);
|
|
224
|
+
res.end();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (req.method === 'POST' && req.url === '/commit') {
|
|
229
|
+
let body = '';
|
|
230
|
+
req.on('data', chunk => { body += chunk; });
|
|
231
|
+
req.on('end', () => {
|
|
232
|
+
try {
|
|
233
|
+
const updatedTokens = JSON.parse(body);
|
|
234
|
+
writeFileSync(TOKENS_PATH, JSON.stringify(updatedTokens, null, 2) + '\n', 'utf-8');
|
|
235
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
236
|
+
res.end(JSON.stringify({ ok: true, message: 'Tokens committed to disk.' }));
|
|
237
|
+
console.log('[BlueDither] Tokens committed successfully.');
|
|
238
|
+
} catch (e) {
|
|
239
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
240
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (req.method === 'GET' && req.url === '/tokens') {
|
|
247
|
+
const tokens = loadTokens();
|
|
248
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
249
|
+
res.end(JSON.stringify(tokens));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
|
|
254
|
+
try {
|
|
255
|
+
const tokens = loadTokens();
|
|
256
|
+
const page = renderFullPage(tokens);
|
|
257
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
258
|
+
res.end(page);
|
|
259
|
+
} catch (e) {
|
|
260
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
261
|
+
res.end('Server error: ' + e.message + '\n' + e.stack);
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Static files from assets
|
|
267
|
+
const filePath = resolve(THEME_DIR, 'assets', req.url.slice(1));
|
|
268
|
+
try {
|
|
269
|
+
const data = readFileSync(filePath);
|
|
270
|
+
const ext = extname(filePath);
|
|
271
|
+
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
|
|
272
|
+
res.end(data);
|
|
273
|
+
} catch {
|
|
274
|
+
res.writeHead(404);
|
|
275
|
+
res.end('Not found');
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
server.listen(PORT, () => {
|
|
280
|
+
console.log(`\n BlueDither Fine-Tuner running at http://localhost:${PORT}\n`);
|
|
281
|
+
console.log(` Tokens: ${TOKENS_PATH}`);
|
|
282
|
+
console.log(` Edit in browser → hit Commit → tokens.json updated on disk.\n`);
|
|
283
|
+
});
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/* =============================================
|
|
2
|
+
BlueDither Fine-Tuner — Overlay Panel v2
|
|
3
|
+
============================================= */
|
|
4
|
+
|
|
5
|
+
#bd-tuner {
|
|
6
|
+
position: fixed;
|
|
7
|
+
top: 16px;
|
|
8
|
+
right: 16px;
|
|
9
|
+
width: 320px;
|
|
10
|
+
max-height: calc(100vh - 32px);
|
|
11
|
+
overflow-y: auto;
|
|
12
|
+
background: rgba(10, 10, 20, 0.92);
|
|
13
|
+
backdrop-filter: blur(16px);
|
|
14
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
15
|
+
border-radius: 12px;
|
|
16
|
+
padding: 20px;
|
|
17
|
+
z-index: 9999;
|
|
18
|
+
font-family: "SF Mono", "Cascadia Code", "Consolas", monospace;
|
|
19
|
+
font-size: 12px;
|
|
20
|
+
color: #e0e0e0;
|
|
21
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
|
22
|
+
transition: transform 0.2s ease;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#bd-tuner.collapsed {
|
|
26
|
+
transform: translateX(calc(100% + 32px));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#bd-tuner-toggle {
|
|
30
|
+
position: fixed;
|
|
31
|
+
top: 16px;
|
|
32
|
+
right: 16px;
|
|
33
|
+
z-index: 10000;
|
|
34
|
+
background: rgba(10, 10, 20, 0.92);
|
|
35
|
+
backdrop-filter: blur(16px);
|
|
36
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
37
|
+
border-radius: 8px;
|
|
38
|
+
color: #e0e0e0;
|
|
39
|
+
padding: 8px 12px;
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
font-family: "SF Mono", "Cascadia Code", "Consolas", monospace;
|
|
42
|
+
font-size: 11px;
|
|
43
|
+
letter-spacing: 0.05em;
|
|
44
|
+
text-transform: uppercase;
|
|
45
|
+
display: none;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#bd-tuner.collapsed ~ #bd-tuner-toggle {
|
|
49
|
+
display: block;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.bd-tuner-title {
|
|
53
|
+
font-size: 11px;
|
|
54
|
+
font-weight: 600;
|
|
55
|
+
text-transform: uppercase;
|
|
56
|
+
letter-spacing: 0.1em;
|
|
57
|
+
color: #888;
|
|
58
|
+
margin-bottom: 16px;
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
justify-content: space-between;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.bd-tuner-title button {
|
|
65
|
+
background: none;
|
|
66
|
+
border: none;
|
|
67
|
+
color: #666;
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
font-size: 16px;
|
|
70
|
+
line-height: 1;
|
|
71
|
+
padding: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.bd-tuner-title button:hover {
|
|
75
|
+
color: #fff;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#bd-tuner-reset {
|
|
79
|
+
font-size: 18px !important;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.bd-tuner-section {
|
|
83
|
+
margin-bottom: 16px;
|
|
84
|
+
padding-bottom: 16px;
|
|
85
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.bd-tuner-section:last-of-type {
|
|
89
|
+
border-bottom: none;
|
|
90
|
+
margin-bottom: 8px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.bd-tuner-section-label {
|
|
94
|
+
font-size: 10px;
|
|
95
|
+
text-transform: uppercase;
|
|
96
|
+
letter-spacing: 0.12em;
|
|
97
|
+
color: #666;
|
|
98
|
+
margin-bottom: 10px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* ── Row layouts ── */
|
|
102
|
+
|
|
103
|
+
.bd-tuner-row {
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: space-between;
|
|
107
|
+
margin-bottom: 8px;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.bd-tuner-row:last-child {
|
|
111
|
+
margin-bottom: 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.bd-tuner-row-stacked {
|
|
115
|
+
flex-direction: column;
|
|
116
|
+
align-items: stretch;
|
|
117
|
+
gap: 4px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.bd-tuner-row-top {
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
justify-content: space-between;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.bd-tuner-label {
|
|
127
|
+
font-size: 11px;
|
|
128
|
+
color: #999;
|
|
129
|
+
flex-shrink: 0;
|
|
130
|
+
width: 110px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.bd-tuner-value {
|
|
134
|
+
font-size: 10px;
|
|
135
|
+
color: #666;
|
|
136
|
+
text-align: right;
|
|
137
|
+
flex-shrink: 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.bd-tuner-input {
|
|
141
|
+
flex: 1;
|
|
142
|
+
min-width: 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* ── Color input ── */
|
|
146
|
+
|
|
147
|
+
.bd-tuner-input input[type="color"] {
|
|
148
|
+
width: 32px;
|
|
149
|
+
height: 24px;
|
|
150
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
151
|
+
border-radius: 4px;
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
background: none;
|
|
154
|
+
padding: 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* ── Slider ── */
|
|
158
|
+
|
|
159
|
+
.bd-tuner-slider {
|
|
160
|
+
width: 100%;
|
|
161
|
+
height: 4px;
|
|
162
|
+
-webkit-appearance: none;
|
|
163
|
+
appearance: none;
|
|
164
|
+
background: rgba(255, 255, 255, 0.1);
|
|
165
|
+
border-radius: 2px;
|
|
166
|
+
outline: none;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.bd-tuner-slider::-webkit-slider-thumb {
|
|
170
|
+
-webkit-appearance: none;
|
|
171
|
+
width: 12px;
|
|
172
|
+
height: 12px;
|
|
173
|
+
border-radius: 50%;
|
|
174
|
+
background: #fff;
|
|
175
|
+
cursor: pointer;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* ── Px input with number + unit ── */
|
|
179
|
+
|
|
180
|
+
.bd-tuner-px-input-wrap {
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
gap: 2px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.bd-tuner-px-num {
|
|
187
|
+
width: 56px;
|
|
188
|
+
background: rgba(255, 255, 255, 0.06);
|
|
189
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
190
|
+
border-radius: 4px;
|
|
191
|
+
color: #e0e0e0;
|
|
192
|
+
padding: 3px 6px;
|
|
193
|
+
font-family: inherit;
|
|
194
|
+
font-size: 11px;
|
|
195
|
+
outline: none;
|
|
196
|
+
text-align: right;
|
|
197
|
+
-moz-appearance: textfield;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.bd-tuner-px-num::-webkit-inner-spin-button,
|
|
201
|
+
.bd-tuner-px-num::-webkit-outer-spin-button {
|
|
202
|
+
-webkit-appearance: none;
|
|
203
|
+
margin: 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.bd-tuner-px-num:focus {
|
|
207
|
+
border-color: rgba(51, 0, 255, 0.5);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.bd-tuner-unit {
|
|
211
|
+
font-size: 10px;
|
|
212
|
+
color: #555;
|
|
213
|
+
width: 16px;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* ── Font dropdown ── */
|
|
217
|
+
|
|
218
|
+
.bd-tuner-font-input {
|
|
219
|
+
position: relative;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.bd-tuner-font-search {
|
|
223
|
+
width: 100%;
|
|
224
|
+
background: rgba(255, 255, 255, 0.06);
|
|
225
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
226
|
+
border-radius: 4px;
|
|
227
|
+
color: #e0e0e0;
|
|
228
|
+
padding: 5px 8px;
|
|
229
|
+
font-family: inherit;
|
|
230
|
+
font-size: 11px;
|
|
231
|
+
outline: none;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.bd-tuner-font-search:focus {
|
|
235
|
+
border-color: rgba(51, 0, 255, 0.5);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.bd-tuner-font-dropdown {
|
|
239
|
+
display: none;
|
|
240
|
+
position: absolute;
|
|
241
|
+
top: 100%;
|
|
242
|
+
left: 0;
|
|
243
|
+
right: 0;
|
|
244
|
+
max-height: 200px;
|
|
245
|
+
overflow-y: auto;
|
|
246
|
+
background: rgba(15, 15, 25, 0.98);
|
|
247
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
248
|
+
border-radius: 4px;
|
|
249
|
+
margin-top: 2px;
|
|
250
|
+
z-index: 100;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.bd-tuner-font-dropdown.open {
|
|
254
|
+
display: block;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.bd-tuner-font-option {
|
|
258
|
+
padding: 6px 8px;
|
|
259
|
+
font-size: 13px;
|
|
260
|
+
cursor: pointer;
|
|
261
|
+
white-space: nowrap;
|
|
262
|
+
overflow: hidden;
|
|
263
|
+
text-overflow: ellipsis;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.bd-tuner-font-option:hover {
|
|
267
|
+
background: rgba(51, 0, 255, 0.3);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.bd-tuner-font-dropdown::-webkit-scrollbar {
|
|
271
|
+
width: 4px;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.bd-tuner-font-dropdown::-webkit-scrollbar-thumb {
|
|
275
|
+
background: rgba(255, 255, 255, 0.1);
|
|
276
|
+
border-radius: 2px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* ── Segmented buttons ── */
|
|
280
|
+
|
|
281
|
+
.bd-tuner-segmented {
|
|
282
|
+
display: flex;
|
|
283
|
+
gap: 1px;
|
|
284
|
+
background: rgba(255, 255, 255, 0.06);
|
|
285
|
+
border-radius: 4px;
|
|
286
|
+
overflow: hidden;
|
|
287
|
+
margin-top: 4px;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.bd-tuner-seg-btn {
|
|
291
|
+
flex: 1;
|
|
292
|
+
padding: 5px 4px;
|
|
293
|
+
background: transparent;
|
|
294
|
+
border: none;
|
|
295
|
+
color: #888;
|
|
296
|
+
font-family: inherit;
|
|
297
|
+
font-size: 10px;
|
|
298
|
+
cursor: pointer;
|
|
299
|
+
text-transform: capitalize;
|
|
300
|
+
transition: background 0.1s, color 0.1s;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.bd-tuner-seg-btn:hover {
|
|
304
|
+
background: rgba(255, 255, 255, 0.06);
|
|
305
|
+
color: #ccc;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.bd-tuner-seg-btn.active {
|
|
309
|
+
background: rgba(51, 0, 255, 0.5);
|
|
310
|
+
color: #fff;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/* ── Text inputs ── */
|
|
314
|
+
|
|
315
|
+
.bd-tuner-input input[type="text"],
|
|
316
|
+
.bd-tuner-input input[type="number"] {
|
|
317
|
+
width: 100%;
|
|
318
|
+
background: rgba(255, 255, 255, 0.06);
|
|
319
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
320
|
+
border-radius: 4px;
|
|
321
|
+
color: #e0e0e0;
|
|
322
|
+
padding: 4px 8px;
|
|
323
|
+
font-family: inherit;
|
|
324
|
+
font-size: 11px;
|
|
325
|
+
outline: none;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.bd-tuner-input input[type="text"]:focus,
|
|
329
|
+
.bd-tuner-input input[type="number"]:focus {
|
|
330
|
+
border-color: rgba(51, 0, 255, 0.5);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* ── Commit button ── */
|
|
334
|
+
|
|
335
|
+
#bd-tuner-commit {
|
|
336
|
+
width: 100%;
|
|
337
|
+
padding: 10px;
|
|
338
|
+
background: #3300FF;
|
|
339
|
+
color: #fff;
|
|
340
|
+
border: none;
|
|
341
|
+
border-radius: 6px;
|
|
342
|
+
font-family: inherit;
|
|
343
|
+
font-size: 12px;
|
|
344
|
+
font-weight: 600;
|
|
345
|
+
text-transform: uppercase;
|
|
346
|
+
letter-spacing: 0.08em;
|
|
347
|
+
cursor: pointer;
|
|
348
|
+
transition: background 0.15s ease;
|
|
349
|
+
margin-top: 8px;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#bd-tuner-commit:hover { background: #4400FF; }
|
|
353
|
+
#bd-tuner-commit:active { background: #2200CC; }
|
|
354
|
+
#bd-tuner-commit.success { background: #0a7; }
|
|
355
|
+
|
|
356
|
+
/* ── Scrollbar ── */
|
|
357
|
+
|
|
358
|
+
#bd-tuner::-webkit-scrollbar { width: 4px; }
|
|
359
|
+
#bd-tuner::-webkit-scrollbar-thumb {
|
|
360
|
+
background: rgba(255, 255, 255, 0.1);
|
|
361
|
+
border-radius: 2px;
|
|
362
|
+
}
|