bluedither 1.0.0
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/bluedither.config.json +14 -0
- package/cli/bin.js +60 -0
- package/cli/commands/extract.js +103 -0
- package/cli/commands/install.js +126 -0
- package/cli/commands/publish.js +85 -0
- package/cli/commands/tune.js +63 -0
- package/dist/bluedither-shader.js +4867 -0
- package/dist/bluedither-tuner.js +401 -0
- package/lib/render.js +108 -0
- package/package.json +40 -0
- package/skill.md +122 -0
- package/theme/generators/react.md +88 -0
- package/theme/generators/svelte.md +103 -0
- package/theme/generators/vanilla.md +54 -0
- package/theme/generators/vue.md +104 -0
- package/theme/rules.md +71 -0
- package/theme/shaders/bluedither-shader.js +92 -0
- package/theme/shaders/paper-shaders-bundle.js +6159 -0
- package/theme/structure.json +167 -0
- package/theme/template/index.html +235 -0
- package/theme/tokens.defaults.json +81 -0
- package/theme/tokens.json +81 -0
- package/theme/tokens.schema.json +166 -0
|
@@ -0,0 +1,401 @@
|
|
|
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=`
|
|
2
|
+
<div class="bd-tuner-title">
|
|
3
|
+
<span>BlueDither Tuner</span>
|
|
4
|
+
<div style="display:flex;gap:8px;align-items:center;">
|
|
5
|
+
<button id="bd-tuner-reset" title="Reset to defaults">↻</button>
|
|
6
|
+
<button id="bd-tuner-close" title="Close">×</button>
|
|
7
|
+
</div>
|
|
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=`
|
|
10
|
+
<span class="bd-tuner-label">${t}</span>
|
|
11
|
+
<span class="bd-tuner-input"><input type="color" value="${s.substring(0,7)}"></span>
|
|
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=`
|
|
14
|
+
<div class="bd-tuner-row-top">
|
|
15
|
+
<span class="bd-tuner-label">${t}</span>
|
|
16
|
+
<div class="bd-tuner-px-input-wrap">
|
|
17
|
+
<input type="number" class="bd-tuner-px-num" value="${i}" min="${a}" max="${l}" step="${s}">
|
|
18
|
+
<span class="bd-tuner-unit">px</span>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
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=`
|
|
23
|
+
<div class="bd-tuner-row-top">
|
|
24
|
+
<span class="bd-tuner-label">${t}</span>
|
|
25
|
+
<span class="bd-tuner-value">${i}</span>
|
|
26
|
+
</div>
|
|
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=`
|
|
29
|
+
<span class="bd-tuner-label">${t}</span>
|
|
30
|
+
<span class="bd-tuner-input bd-tuner-font-input">
|
|
31
|
+
<input type="text" class="bd-tuner-font-search" value="${l}" placeholder="Search fonts...">
|
|
32
|
+
<div class="bd-tuner-font-dropdown"></div>
|
|
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=`
|
|
35
|
+
<span class="bd-tuner-label">${t}</span>
|
|
36
|
+
<div class="bd-tuner-segmented">
|
|
37
|
+
${a.map(i=>`<button class="bd-tuner-seg-btn${i===s?" active":""}" data-val="${i}">${i}</button>`).join("")}
|
|
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=`/* =============================================
|
|
40
|
+
BlueDither Fine-Tuner \u2014 Overlay Panel v2
|
|
41
|
+
============================================= */
|
|
42
|
+
|
|
43
|
+
#bd-tuner {
|
|
44
|
+
position: fixed;
|
|
45
|
+
top: 16px;
|
|
46
|
+
right: 16px;
|
|
47
|
+
width: 320px;
|
|
48
|
+
max-height: calc(100vh - 32px);
|
|
49
|
+
overflow-y: auto;
|
|
50
|
+
background: rgba(10, 10, 20, 0.92);
|
|
51
|
+
backdrop-filter: blur(16px);
|
|
52
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
53
|
+
border-radius: 12px;
|
|
54
|
+
padding: 20px;
|
|
55
|
+
z-index: 9999;
|
|
56
|
+
font-family: "SF Mono", "Cascadia Code", "Consolas", monospace;
|
|
57
|
+
font-size: 12px;
|
|
58
|
+
color: #e0e0e0;
|
|
59
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
|
60
|
+
transition: transform 0.2s ease;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#bd-tuner.collapsed {
|
|
64
|
+
transform: translateX(calc(100% + 32px));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#bd-tuner-toggle {
|
|
68
|
+
position: fixed;
|
|
69
|
+
top: 16px;
|
|
70
|
+
right: 16px;
|
|
71
|
+
z-index: 10000;
|
|
72
|
+
background: rgba(10, 10, 20, 0.92);
|
|
73
|
+
backdrop-filter: blur(16px);
|
|
74
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
75
|
+
border-radius: 8px;
|
|
76
|
+
color: #e0e0e0;
|
|
77
|
+
padding: 8px 12px;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
font-family: "SF Mono", "Cascadia Code", "Consolas", monospace;
|
|
80
|
+
font-size: 11px;
|
|
81
|
+
letter-spacing: 0.05em;
|
|
82
|
+
text-transform: uppercase;
|
|
83
|
+
display: none;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#bd-tuner.collapsed ~ #bd-tuner-toggle {
|
|
87
|
+
display: block;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.bd-tuner-title {
|
|
91
|
+
font-size: 11px;
|
|
92
|
+
font-weight: 600;
|
|
93
|
+
text-transform: uppercase;
|
|
94
|
+
letter-spacing: 0.1em;
|
|
95
|
+
color: #888;
|
|
96
|
+
margin-bottom: 16px;
|
|
97
|
+
display: flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
justify-content: space-between;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.bd-tuner-title button {
|
|
103
|
+
background: none;
|
|
104
|
+
border: none;
|
|
105
|
+
color: #666;
|
|
106
|
+
cursor: pointer;
|
|
107
|
+
font-size: 16px;
|
|
108
|
+
line-height: 1;
|
|
109
|
+
padding: 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.bd-tuner-title button:hover {
|
|
113
|
+
color: #fff;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#bd-tuner-reset {
|
|
117
|
+
font-size: 18px !important;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.bd-tuner-section {
|
|
121
|
+
margin-bottom: 16px;
|
|
122
|
+
padding-bottom: 16px;
|
|
123
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.bd-tuner-section:last-of-type {
|
|
127
|
+
border-bottom: none;
|
|
128
|
+
margin-bottom: 8px;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.bd-tuner-section-label {
|
|
132
|
+
font-size: 10px;
|
|
133
|
+
text-transform: uppercase;
|
|
134
|
+
letter-spacing: 0.12em;
|
|
135
|
+
color: #666;
|
|
136
|
+
margin-bottom: 10px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* \u2500\u2500 Row layouts \u2500\u2500 */
|
|
140
|
+
|
|
141
|
+
.bd-tuner-row {
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
justify-content: space-between;
|
|
145
|
+
margin-bottom: 8px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.bd-tuner-row:last-child {
|
|
149
|
+
margin-bottom: 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.bd-tuner-row-stacked {
|
|
153
|
+
flex-direction: column;
|
|
154
|
+
align-items: stretch;
|
|
155
|
+
gap: 4px;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.bd-tuner-row-top {
|
|
159
|
+
display: flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
justify-content: space-between;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.bd-tuner-label {
|
|
165
|
+
font-size: 11px;
|
|
166
|
+
color: #999;
|
|
167
|
+
flex-shrink: 0;
|
|
168
|
+
width: 110px;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.bd-tuner-value {
|
|
172
|
+
font-size: 10px;
|
|
173
|
+
color: #666;
|
|
174
|
+
text-align: right;
|
|
175
|
+
flex-shrink: 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.bd-tuner-input {
|
|
179
|
+
flex: 1;
|
|
180
|
+
min-width: 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* \u2500\u2500 Color input \u2500\u2500 */
|
|
184
|
+
|
|
185
|
+
.bd-tuner-input input[type="color"] {
|
|
186
|
+
width: 32px;
|
|
187
|
+
height: 24px;
|
|
188
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
189
|
+
border-radius: 4px;
|
|
190
|
+
cursor: pointer;
|
|
191
|
+
background: none;
|
|
192
|
+
padding: 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* \u2500\u2500 Slider \u2500\u2500 */
|
|
196
|
+
|
|
197
|
+
.bd-tuner-slider {
|
|
198
|
+
width: 100%;
|
|
199
|
+
height: 4px;
|
|
200
|
+
-webkit-appearance: none;
|
|
201
|
+
appearance: none;
|
|
202
|
+
background: rgba(255, 255, 255, 0.1);
|
|
203
|
+
border-radius: 2px;
|
|
204
|
+
outline: none;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.bd-tuner-slider::-webkit-slider-thumb {
|
|
208
|
+
-webkit-appearance: none;
|
|
209
|
+
width: 12px;
|
|
210
|
+
height: 12px;
|
|
211
|
+
border-radius: 50%;
|
|
212
|
+
background: #fff;
|
|
213
|
+
cursor: pointer;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* \u2500\u2500 Px input with number + unit \u2500\u2500 */
|
|
217
|
+
|
|
218
|
+
.bd-tuner-px-input-wrap {
|
|
219
|
+
display: flex;
|
|
220
|
+
align-items: center;
|
|
221
|
+
gap: 2px;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.bd-tuner-px-num {
|
|
225
|
+
width: 56px;
|
|
226
|
+
background: rgba(255, 255, 255, 0.06);
|
|
227
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
228
|
+
border-radius: 4px;
|
|
229
|
+
color: #e0e0e0;
|
|
230
|
+
padding: 3px 6px;
|
|
231
|
+
font-family: inherit;
|
|
232
|
+
font-size: 11px;
|
|
233
|
+
outline: none;
|
|
234
|
+
text-align: right;
|
|
235
|
+
-moz-appearance: textfield;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.bd-tuner-px-num::-webkit-inner-spin-button,
|
|
239
|
+
.bd-tuner-px-num::-webkit-outer-spin-button {
|
|
240
|
+
-webkit-appearance: none;
|
|
241
|
+
margin: 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.bd-tuner-px-num:focus {
|
|
245
|
+
border-color: rgba(51, 0, 255, 0.5);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.bd-tuner-unit {
|
|
249
|
+
font-size: 10px;
|
|
250
|
+
color: #555;
|
|
251
|
+
width: 16px;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* \u2500\u2500 Font dropdown \u2500\u2500 */
|
|
255
|
+
|
|
256
|
+
.bd-tuner-font-input {
|
|
257
|
+
position: relative;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.bd-tuner-font-search {
|
|
261
|
+
width: 100%;
|
|
262
|
+
background: rgba(255, 255, 255, 0.06);
|
|
263
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
264
|
+
border-radius: 4px;
|
|
265
|
+
color: #e0e0e0;
|
|
266
|
+
padding: 5px 8px;
|
|
267
|
+
font-family: inherit;
|
|
268
|
+
font-size: 11px;
|
|
269
|
+
outline: none;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.bd-tuner-font-search:focus {
|
|
273
|
+
border-color: rgba(51, 0, 255, 0.5);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.bd-tuner-font-dropdown {
|
|
277
|
+
display: none;
|
|
278
|
+
position: absolute;
|
|
279
|
+
top: 100%;
|
|
280
|
+
left: 0;
|
|
281
|
+
right: 0;
|
|
282
|
+
max-height: 200px;
|
|
283
|
+
overflow-y: auto;
|
|
284
|
+
background: rgba(15, 15, 25, 0.98);
|
|
285
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
286
|
+
border-radius: 4px;
|
|
287
|
+
margin-top: 2px;
|
|
288
|
+
z-index: 100;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.bd-tuner-font-dropdown.open {
|
|
292
|
+
display: block;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.bd-tuner-font-option {
|
|
296
|
+
padding: 6px 8px;
|
|
297
|
+
font-size: 13px;
|
|
298
|
+
cursor: pointer;
|
|
299
|
+
white-space: nowrap;
|
|
300
|
+
overflow: hidden;
|
|
301
|
+
text-overflow: ellipsis;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.bd-tuner-font-option:hover {
|
|
305
|
+
background: rgba(51, 0, 255, 0.3);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.bd-tuner-font-dropdown::-webkit-scrollbar {
|
|
309
|
+
width: 4px;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.bd-tuner-font-dropdown::-webkit-scrollbar-thumb {
|
|
313
|
+
background: rgba(255, 255, 255, 0.1);
|
|
314
|
+
border-radius: 2px;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* \u2500\u2500 Segmented buttons \u2500\u2500 */
|
|
318
|
+
|
|
319
|
+
.bd-tuner-segmented {
|
|
320
|
+
display: flex;
|
|
321
|
+
gap: 1px;
|
|
322
|
+
background: rgba(255, 255, 255, 0.06);
|
|
323
|
+
border-radius: 4px;
|
|
324
|
+
overflow: hidden;
|
|
325
|
+
margin-top: 4px;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.bd-tuner-seg-btn {
|
|
329
|
+
flex: 1;
|
|
330
|
+
padding: 5px 4px;
|
|
331
|
+
background: transparent;
|
|
332
|
+
border: none;
|
|
333
|
+
color: #888;
|
|
334
|
+
font-family: inherit;
|
|
335
|
+
font-size: 10px;
|
|
336
|
+
cursor: pointer;
|
|
337
|
+
text-transform: capitalize;
|
|
338
|
+
transition: background 0.1s, color 0.1s;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.bd-tuner-seg-btn:hover {
|
|
342
|
+
background: rgba(255, 255, 255, 0.06);
|
|
343
|
+
color: #ccc;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.bd-tuner-seg-btn.active {
|
|
347
|
+
background: rgba(51, 0, 255, 0.5);
|
|
348
|
+
color: #fff;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/* \u2500\u2500 Text inputs \u2500\u2500 */
|
|
352
|
+
|
|
353
|
+
.bd-tuner-input input[type="text"],
|
|
354
|
+
.bd-tuner-input input[type="number"] {
|
|
355
|
+
width: 100%;
|
|
356
|
+
background: rgba(255, 255, 255, 0.06);
|
|
357
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
358
|
+
border-radius: 4px;
|
|
359
|
+
color: #e0e0e0;
|
|
360
|
+
padding: 4px 8px;
|
|
361
|
+
font-family: inherit;
|
|
362
|
+
font-size: 11px;
|
|
363
|
+
outline: none;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.bd-tuner-input input[type="text"]:focus,
|
|
367
|
+
.bd-tuner-input input[type="number"]:focus {
|
|
368
|
+
border-color: rgba(51, 0, 255, 0.5);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/* \u2500\u2500 Commit button \u2500\u2500 */
|
|
372
|
+
|
|
373
|
+
#bd-tuner-commit {
|
|
374
|
+
width: 100%;
|
|
375
|
+
padding: 10px;
|
|
376
|
+
background: #3300FF;
|
|
377
|
+
color: #fff;
|
|
378
|
+
border: none;
|
|
379
|
+
border-radius: 6px;
|
|
380
|
+
font-family: inherit;
|
|
381
|
+
font-size: 12px;
|
|
382
|
+
font-weight: 600;
|
|
383
|
+
text-transform: uppercase;
|
|
384
|
+
letter-spacing: 0.08em;
|
|
385
|
+
cursor: pointer;
|
|
386
|
+
transition: background 0.15s ease;
|
|
387
|
+
margin-top: 8px;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
#bd-tuner-commit:hover { background: #4400FF; }
|
|
391
|
+
#bd-tuner-commit:active { background: #2200CC; }
|
|
392
|
+
#bd-tuner-commit.success { background: #0a7; }
|
|
393
|
+
|
|
394
|
+
/* \u2500\u2500 Scrollbar \u2500\u2500 */
|
|
395
|
+
|
|
396
|
+
#bd-tuner::-webkit-scrollbar { width: 4px; }
|
|
397
|
+
#bd-tuner::-webkit-scrollbar-thumb {
|
|
398
|
+
background: rgba(255, 255, 255, 0.1);
|
|
399
|
+
border-radius: 2px;
|
|
400
|
+
}
|
|
401
|
+
`;document.head.appendChild(R);})();
|
package/lib/render.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared rendering utilities for BlueDither.
|
|
3
|
+
* Extracted from fine-tuner/server.js for CLI and build reuse.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Compute a clamp() value from a reference px size.
|
|
8
|
+
* @param {number} px - Reference pixel value
|
|
9
|
+
* @param {number} designWidth - Design reference width
|
|
10
|
+
* @returns {string} CSS clamp() expression
|
|
11
|
+
*/
|
|
12
|
+
export function pxToClamp(px, designWidth) {
|
|
13
|
+
const maxRem = px / 16;
|
|
14
|
+
const vw = (px / designWidth) * 100;
|
|
15
|
+
const minRem = maxRem * 0.55;
|
|
16
|
+
return `clamp(${minRem.toFixed(4)}rem, ${vw.toFixed(4)}vw, ${maxRem.toFixed(4)}rem)`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a dot-path on an object (e.g., "colors.primary").
|
|
21
|
+
*/
|
|
22
|
+
export function resolveTokenPath(obj, path) {
|
|
23
|
+
return path.split('.').reduce((o, k) => (o && o[k] !== undefined ? o[k] : ''), obj);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build computed clamp values from tokens.
|
|
28
|
+
*/
|
|
29
|
+
export function buildClampValues(tokens) {
|
|
30
|
+
const dw = tokens.layout.designWidth;
|
|
31
|
+
const t = tokens.typography;
|
|
32
|
+
const s = tokens.spacing;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
headline: {
|
|
36
|
+
fontSize: pxToClamp(t.headline.referencePx, dw),
|
|
37
|
+
lineHeight: pxToClamp(t.headline.lineHeightPx, dw),
|
|
38
|
+
},
|
|
39
|
+
subHeadline: {
|
|
40
|
+
fontSize: pxToClamp(t.subHeadline.referencePx, dw),
|
|
41
|
+
lineHeight: pxToClamp(t.subHeadline.lineHeightPx, dw),
|
|
42
|
+
},
|
|
43
|
+
logo: {
|
|
44
|
+
fontSize: pxToClamp(t.logo.referencePx, dw),
|
|
45
|
+
lineHeight: pxToClamp(t.logo.lineHeightPx, dw),
|
|
46
|
+
},
|
|
47
|
+
navItem: {
|
|
48
|
+
fontSize: pxToClamp(t.navItem.referencePx, dw),
|
|
49
|
+
lineHeight: pxToClamp(t.navItem.lineHeightPx, dw),
|
|
50
|
+
},
|
|
51
|
+
ctaButton: {
|
|
52
|
+
fontSize: pxToClamp(t.ctaButton.referencePx, dw),
|
|
53
|
+
lineHeight: pxToClamp(t.ctaButton.lineHeightPx, dw),
|
|
54
|
+
},
|
|
55
|
+
spacing: {
|
|
56
|
+
headerPaddingX: pxToClamp(s.headerPaddingX, dw),
|
|
57
|
+
headerPaddingY: pxToClamp(s.headerPaddingY, dw),
|
|
58
|
+
heroPaddingTop: pxToClamp(s.heroPaddingTop, dw),
|
|
59
|
+
heroPaddingBottom: pxToClamp(s.heroPaddingBottom, dw),
|
|
60
|
+
heroPaddingX: pxToClamp(s.heroPaddingX, dw),
|
|
61
|
+
navGap: pxToClamp(s.navGap, dw),
|
|
62
|
+
ctaPaddingX: pxToClamp(s.ctaPaddingX, dw),
|
|
63
|
+
ctaPaddingY: pxToClamp(s.ctaPaddingY, dw),
|
|
64
|
+
ctaBorderRadius: `${(s.ctaBorderRadius / 16).toFixed(4)}rem`,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Render the template HTML with token values.
|
|
71
|
+
* @param {string} template - Raw template HTML
|
|
72
|
+
* @param {object} tokens - Token values
|
|
73
|
+
* @returns {string} Rendered HTML
|
|
74
|
+
*/
|
|
75
|
+
export function renderTemplate(template, tokens) {
|
|
76
|
+
const clamp = buildClampValues(tokens);
|
|
77
|
+
const renderData = { ...tokens, clamp };
|
|
78
|
+
|
|
79
|
+
// Handle font URL encoding
|
|
80
|
+
template = template.replace(/\{\{typography\.primaryFont\.urlEncoded\}\}/g,
|
|
81
|
+
encodeURIComponent(tokens.typography.primaryFont));
|
|
82
|
+
template = template.replace(/\{\{typography\.secondaryFont\.urlEncoded\}\}/g,
|
|
83
|
+
encodeURIComponent(tokens.typography.secondaryFont));
|
|
84
|
+
|
|
85
|
+
// Handle {{#each}} blocks
|
|
86
|
+
template = template.replace(
|
|
87
|
+
/\{\{#each\s+([^}]+)\}\}([\s\S]*?)\{\{\/each\}\}/g,
|
|
88
|
+
(_, arrayPath, inner) => {
|
|
89
|
+
const arr = resolveTokenPath(renderData, arrayPath.trim());
|
|
90
|
+
if (!Array.isArray(arr)) return '';
|
|
91
|
+
return arr.map(item => inner.replace(/\{\{this\}\}/g, item)).join('\n');
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Replace remaining {{path}} tokens
|
|
96
|
+
template = template.replace(/\{\{([^#/][^}]*)\}\}/g, (_, path) => {
|
|
97
|
+
const val = resolveTokenPath(renderData, path.trim());
|
|
98
|
+
return val !== undefined && val !== '' ? val : '';
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Fix headline newlines
|
|
102
|
+
template = template.replace(
|
|
103
|
+
/(<div class="bd-headline">)([\s\S]*?)(<\/div>)/,
|
|
104
|
+
(_, open, content, close) => open + content.replace(/\n/g, '<br>') + close
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return template;
|
|
108
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bluedither",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A bold, dithered-shader hero theme for Claude Code — skill + fine-tuner",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"bluedither": "./cli/bin.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"tune": "node fine-tuner/server.js",
|
|
11
|
+
"tune:port": "node fine-tuner/server.js --port",
|
|
12
|
+
"build": "node scripts/build.js",
|
|
13
|
+
"validate": "node scripts/validate-tokens.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/",
|
|
17
|
+
"theme/",
|
|
18
|
+
"cli/",
|
|
19
|
+
"lib/",
|
|
20
|
+
"skill.md",
|
|
21
|
+
"bluedither.config.json"
|
|
22
|
+
],
|
|
23
|
+
"keywords": [
|
|
24
|
+
"claude-code",
|
|
25
|
+
"theme",
|
|
26
|
+
"shader",
|
|
27
|
+
"dithering",
|
|
28
|
+
"skill",
|
|
29
|
+
"bluedither"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@paper-design/shaders": "^0.0.71"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"ajv": "^8.18.0",
|
|
37
|
+
"ajv-draft-04": "^1.0.0",
|
|
38
|
+
"esbuild": "^0.27.3"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/skill.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bluedither-theme
|
|
3
|
+
description: Generate a bold hero+navbar layout with a dithered shader background. Detects framework, supports greenfield/partial/design-system modes.
|
|
4
|
+
trigger: bluedither-theme
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# BlueDither Theme — Skill Instructions
|
|
8
|
+
|
|
9
|
+
You are generating a layout using the BlueDither theme. This is a **highly opinionated, structurally locked template**. The structure is defined in `theme/structure.json`, design rules in `theme/rules.md`, and tokens in `theme/tokens.json` (validated by `theme/tokens.schema.json`).
|
|
10
|
+
|
|
11
|
+
## Modes
|
|
12
|
+
|
|
13
|
+
The skill operates in three modes based on the user's request:
|
|
14
|
+
|
|
15
|
+
### 1. Greenfield Mode (default)
|
|
16
|
+
Generate the full layout as framework-native components. This is the default when the user says things like "create a landing page", "generate a theme for my surf shop", etc.
|
|
17
|
+
|
|
18
|
+
### 2. Partial Mode
|
|
19
|
+
Generate only specific components. Triggered when the user says "just the navbar", "only the hero section", "add the header component", etc. Output only the requested components plus the shared CSS file.
|
|
20
|
+
|
|
21
|
+
### 3. Design-System Mode
|
|
22
|
+
Emit only CSS custom properties and design rules — no components. Triggered when the user says "apply this theme to my site", "just the design tokens", "give me the CSS variables", etc. Output a CSS file with all `--bd-*` custom properties and a reference to `theme/rules.md`.
|
|
23
|
+
|
|
24
|
+
## Step 1 — Detect Framework
|
|
25
|
+
|
|
26
|
+
Read the target project's `package.json` to detect the framework:
|
|
27
|
+
|
|
28
|
+
| Signal | Framework |
|
|
29
|
+
|--------|-----------|
|
|
30
|
+
| `react` or `react-dom` in dependencies | React |
|
|
31
|
+
| `next` in dependencies | React (Next.js) |
|
|
32
|
+
| `vue` in dependencies | Vue |
|
|
33
|
+
| `nuxt` in dependencies | Vue (Nuxt) |
|
|
34
|
+
| `svelte` in dependencies | Svelte |
|
|
35
|
+
| `@sveltejs/kit` in dependencies | Svelte (SvelteKit) |
|
|
36
|
+
| None of the above | Vanilla HTML |
|
|
37
|
+
|
|
38
|
+
Also detect TypeScript: look for `typescript` in devDependencies or a `tsconfig.json` file.
|
|
39
|
+
|
|
40
|
+
If no `package.json` exists in the target directory, default to Vanilla HTML.
|
|
41
|
+
|
|
42
|
+
## Step 2 — Determine Content
|
|
43
|
+
|
|
44
|
+
From the user's description (e.g., "a fictional surf shop"), infer appropriate values for these **content slots only**:
|
|
45
|
+
|
|
46
|
+
| Slot | Constraints |
|
|
47
|
+
|------|-------------|
|
|
48
|
+
| `companyName` | Short brand name, ALL CAPS, 1-2 words |
|
|
49
|
+
| `navItems` | Exactly 4 navigation labels, ALL CAPS |
|
|
50
|
+
| `ctaText` | CTA button label, ALL CAPS, 1-2 words |
|
|
51
|
+
| `headline` | Bold headline, ALL CAPS, 2 lines max (~4-6 words), separated by `\n` |
|
|
52
|
+
| `subHeadline` | One sentence, ALL CAPS (CSS handles text-transform) |
|
|
53
|
+
|
|
54
|
+
## Step 3 — Read Theme Files
|
|
55
|
+
|
|
56
|
+
Read these files from the BlueDither theme directory:
|
|
57
|
+
1. `theme/tokens.json` — All design tokens. **Do not modify** any non-content tokens.
|
|
58
|
+
2. `theme/structure.json` — Component tree contract. The generated DOM must match this exactly.
|
|
59
|
+
3. `theme/rules.md` — Design language rules. Follow these when making any design decisions.
|
|
60
|
+
|
|
61
|
+
## Step 4 — Read Generator Instructions
|
|
62
|
+
|
|
63
|
+
Based on the detected framework, read the corresponding generator file:
|
|
64
|
+
- Vanilla: `theme/generators/vanilla.md`
|
|
65
|
+
- React: `theme/generators/react.md`
|
|
66
|
+
- Vue: `theme/generators/vue.md`
|
|
67
|
+
- Svelte: `theme/generators/svelte.md`
|
|
68
|
+
|
|
69
|
+
Follow the generator's patterns for component structure, file naming, shader initialization, and TypeScript usage.
|
|
70
|
+
|
|
71
|
+
## Step 5 — Generate Output
|
|
72
|
+
|
|
73
|
+
### Greenfield Mode
|
|
74
|
+
1. Update ONLY the `content` object in `tokens.json` with generated values
|
|
75
|
+
2. Generate all component files per the generator instructions
|
|
76
|
+
3. Copy shader files to the target project
|
|
77
|
+
4. Generate the CSS custom properties file with computed `clamp()` values
|
|
78
|
+
|
|
79
|
+
### Partial Mode
|
|
80
|
+
1. Generate only the requested component(s) per generator instructions
|
|
81
|
+
2. Always include the CSS file (components depend on it)
|
|
82
|
+
3. Include shader files only if the hero component is requested
|
|
83
|
+
|
|
84
|
+
### Design-System Mode
|
|
85
|
+
1. Generate only `bluedither.css` with all `--bd-*` custom properties
|
|
86
|
+
2. Include a comment header referencing `theme/rules.md` for usage guidance
|
|
87
|
+
3. Do NOT generate any component files
|
|
88
|
+
|
|
89
|
+
## Step 6 — Inform About Fine-Tuner
|
|
90
|
+
|
|
91
|
+
Tell the user:
|
|
92
|
+
> Your BlueDither theme has been generated. To customize colors, fonts, spacing, and shader parameters visually, run:
|
|
93
|
+
>
|
|
94
|
+
> ```
|
|
95
|
+
> npm run tune
|
|
96
|
+
> ```
|
|
97
|
+
>
|
|
98
|
+
> Open http://localhost:3333 to see your theme with a floating customization panel.
|
|
99
|
+
|
|
100
|
+
## CSS Custom Property Computation
|
|
101
|
+
|
|
102
|
+
All `referencePx` and spacing values must be converted to `clamp()`:
|
|
103
|
+
```
|
|
104
|
+
max = px / 16 (rem)
|
|
105
|
+
vw = px / designWidth * 100
|
|
106
|
+
min = max * 0.55
|
|
107
|
+
→ clamp(min rem, vw vw, max rem)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Border radius converts directly: `px / 16` rem (no clamp).
|
|
111
|
+
|
|
112
|
+
## Critical Rules
|
|
113
|
+
|
|
114
|
+
1. **Structure is locked** — the DOM must match `structure.json`. No extra elements.
|
|
115
|
+
2. **Only content slots are editable** — never change colors, typography, spacing, shader, or opacity tokens during generation.
|
|
116
|
+
3. **Two sections only** — navbar + hero. No footers, sidebars, additional sections, icons, or images.
|
|
117
|
+
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.
|
|
119
|
+
6. **CSS namespace** — all custom properties use `--bd-*` prefix. All classes use `bd-*` prefix.
|
|
120
|
+
7. **Headline is 2 lines max**, ALL CAPS, 4-6 words.
|
|
121
|
+
8. **Exactly 4 nav items.**
|
|
122
|
+
9. **The fine-tuner is the ONLY way to customize visual tokens** after generation.
|