jekyll-theme-zer0 0.22.0 → 0.22.19

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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +236 -0
  3. data/README.md +66 -19
  4. data/_data/navigation/admin.yml +53 -0
  5. data/_data/theme_backgrounds.yml +121 -0
  6. data/_includes/components/admin-tabs.html +59 -0
  7. data/_includes/components/analytics-dashboard.html +232 -0
  8. data/_includes/components/background-customizer.html +159 -0
  9. data/_includes/components/background-settings.html +137 -0
  10. data/_includes/components/collection-manager.html +151 -0
  11. data/_includes/components/component-showcase.html +452 -0
  12. data/_includes/components/config-editor.html +207 -0
  13. data/_includes/components/config-viewer.html +479 -0
  14. data/_includes/components/env-dashboard.html +154 -0
  15. data/_includes/components/feature-card.html +94 -0
  16. data/_includes/components/info-section.html +172 -149
  17. data/_includes/components/js-cdn.html +4 -1
  18. data/_includes/components/nav-editor.html +99 -0
  19. data/_includes/components/setup-banner.html +28 -0
  20. data/_includes/components/setup-check.html +53 -0
  21. data/_includes/components/svg-background.html +42 -0
  22. data/_includes/components/theme-customizer.html +46 -0
  23. data/_includes/content/seo.html +68 -135
  24. data/_includes/core/footer.html +1 -1
  25. data/_includes/core/head.html +3 -2
  26. data/_includes/core/header.html +14 -7
  27. data/_includes/landing/landing-install-cards.html +18 -7
  28. data/_includes/navigation/admin-nav.html +95 -0
  29. data/_includes/navigation/navbar.html +43 -5
  30. data/_includes/navigation/sidebar-left.html +1 -1
  31. data/_includes/setup/wizard.html +330 -0
  32. data/_layouts/admin.html +166 -0
  33. data/_layouts/landing.html +23 -9
  34. data/_layouts/root.html +12 -6
  35. data/_layouts/setup.html +73 -0
  36. data/_plugins/preview_image_generator.rb +26 -12
  37. data/_sass/core/_navbar.scss +2 -2
  38. data/_sass/custom.scss +28 -6
  39. data/_sass/theme/_background-mixins.scss +95 -0
  40. data/_sass/theme/_backgrounds.scss +156 -0
  41. data/_sass/theme/_color-modes.scss +2 -1
  42. data/assets/backgrounds/gradients/air.svg +15 -0
  43. data/assets/backgrounds/gradients/aqua.svg +15 -0
  44. data/assets/backgrounds/gradients/contrast.svg +15 -0
  45. data/assets/backgrounds/gradients/dark.svg +15 -0
  46. data/assets/backgrounds/gradients/dirt.svg +15 -0
  47. data/assets/backgrounds/gradients/mint.svg +15 -0
  48. data/assets/backgrounds/gradients/neon.svg +15 -0
  49. data/assets/backgrounds/gradients/plum.svg +15 -0
  50. data/assets/backgrounds/gradients/sunrise.svg +15 -0
  51. data/assets/backgrounds/noise/air.svg +8 -0
  52. data/assets/backgrounds/noise/aqua.svg +8 -0
  53. data/assets/backgrounds/noise/contrast.svg +8 -0
  54. data/assets/backgrounds/noise/dark.svg +8 -0
  55. data/assets/backgrounds/noise/dirt.svg +8 -0
  56. data/assets/backgrounds/noise/mint.svg +8 -0
  57. data/assets/backgrounds/noise/neon.svg +8 -0
  58. data/assets/backgrounds/noise/plum.svg +8 -0
  59. data/assets/backgrounds/noise/sunrise.svg +8 -0
  60. data/assets/backgrounds/patterns/air.svg +7 -0
  61. data/assets/backgrounds/patterns/aqua.svg +7 -0
  62. data/assets/backgrounds/patterns/contrast.svg +4 -0
  63. data/assets/backgrounds/patterns/dark.svg +5 -0
  64. data/assets/backgrounds/patterns/dirt.svg +5 -0
  65. data/assets/backgrounds/patterns/mint.svg +6 -0
  66. data/assets/backgrounds/patterns/neon.svg +6 -0
  67. data/assets/backgrounds/patterns/plum.svg +6 -0
  68. data/assets/backgrounds/patterns/sunrise.svg +5 -0
  69. data/assets/js/background-customizer.js +73 -0
  70. data/assets/js/code-copy.js +18 -47
  71. data/assets/js/config-utility.js +307 -0
  72. data/assets/js/nav-editor.js +39 -0
  73. data/assets/js/palette-generator.js +415 -0
  74. data/assets/js/search-modal.js +31 -11
  75. data/assets/js/setup-wizard.js +306 -0
  76. data/assets/js/skin-editor.js +645 -0
  77. data/assets/js/theme-customizer.js +102 -0
  78. data/assets/js/ui-enhancements.js +15 -24
  79. data/assets/vendor/bootstrap/css/bootstrap.min.css +1 -0
  80. data/assets/vendor/bootstrap/js/bootstrap.bundle.min.js +1 -0
  81. data/scripts/README.md +45 -0
  82. data/scripts/features/generate-preview-images +297 -7
  83. data/scripts/features/install-preview-generator +51 -33
  84. data/scripts/fork-cleanup.sh +92 -19
  85. data/scripts/github-setup.sh +284 -0
  86. data/scripts/init_setup.sh +0 -1
  87. data/scripts/lib/frontmatter.sh +543 -0
  88. data/scripts/lib/migrate.sh +265 -0
  89. data/scripts/lib/preview_generator.py +607 -32
  90. data/scripts/lint-pages +505 -0
  91. data/scripts/migrate.sh +201 -0
  92. data/scripts/platform/setup-linux.sh +244 -0
  93. data/scripts/platform/setup-macos.sh +187 -0
  94. data/scripts/platform/setup-wsl.sh +196 -0
  95. metadata +71 -6
@@ -0,0 +1,645 @@
1
+ /**
2
+ * skin-editor.js — Colorffy-inspired skin editor for zer0-mistakes theme
3
+ *
4
+ * Edit existing skins or create new ones by adjusting gradient colors,
5
+ * SVG filter parameters, and viewing auto-generated palettes.
6
+ * Changes can be applied live and saved as custom skins in localStorage.
7
+ *
8
+ * Requires: chroma-js (loaded globally)
9
+ * Integrates with: background-customizer.js (zer0Bg API)
10
+ */
11
+ (function () {
12
+ 'use strict';
13
+
14
+ /* ===================================================================
15
+ * Built-in skin definitions — exact hex values from SVG files
16
+ * =================================================================== */
17
+ const BUILTIN_SKINS = {
18
+ air: { stops: ['#e8f4f8', '#b8d4e3', '#6fa8dc'], filter: { freq: 0.006, oct: 4, seed: 2, scale: 60, opacity: 0.5 }, patternSize: 50 },
19
+ aqua: { stops: ['#0077b6', '#00b4d8', '#90e0ef'], filter: { freq: 0.008, oct: 4, seed: 5, scale: 70, opacity: 0.5 }, patternSize: 48 },
20
+ contrast: { stops: ['#111111', '#333333', '#ffcc00'], filter: { freq: 0.005, oct: 3, seed: 8, scale: 50, opacity: 0.4 }, patternSize: 40 },
21
+ dark: { stops: ['#1a1a2e', '#16213e', '#0f3460'], filter: { freq: 0.007, oct: 4, seed: 12, scale: 55, opacity: 0.5 }, patternSize: 60 },
22
+ dirt: { stops: ['#5c4033', '#8b6914', '#d4a574'], filter: { freq: 0.009, oct: 4, seed: 20, scale: 65, opacity: 0.5 }, patternSize: 50 },
23
+ neon: { stops: ['#ff006e', '#8338ec', '#3a86ff'], filter: { freq: 0.006, oct: 5, seed: 42, scale: 80, opacity: 0.6 }, patternSize: 48 },
24
+ mint: { stops: ['#2d6a4f', '#52b788', '#95d5b2'], filter: { freq: 0.007, oct: 4, seed: 30, scale: 60, opacity: 0.5 }, patternSize: 50 },
25
+ plum: { stops: ['#4a0e4e', '#812f85', '#c77dff'], filter: { freq: 0.006, oct: 4, seed: 15, scale: 65, opacity: 0.5 }, patternSize: 55 },
26
+ sunrise: { stops: ['#ff6b35', '#f7c59f', '#efefd0'], filter: { freq: 0.008, oct: 4, seed: 7, scale: 60, opacity: 0.5 }, patternSize: 50 }
27
+ };
28
+
29
+ const STORAGE_KEY = 'zer0-custom-skins';
30
+ const STOP_LABELS = ['Start (0%)', 'Middle (50%)', 'End (100%)'];
31
+
32
+ /* ===================================================================
33
+ * Editor state
34
+ * =================================================================== */
35
+ var state = {
36
+ baseSkin: 'aqua',
37
+ isCustom: false,
38
+ stops: ['#0077b6', '#00b4d8', '#90e0ef'],
39
+ filter: { freq: 0.008, oct: 4, seed: 5, scale: 70, opacity: 0.5 },
40
+ patternSize: 48,
41
+ customSkins: {}
42
+ };
43
+
44
+ /* ===================================================================
45
+ * SVG generators
46
+ * =================================================================== */
47
+ function gradientSVG(stops, f) {
48
+ return '<svg xmlns="http://www.w3.org/2000/svg" width="800" height="400" viewBox="0 0 800 400">' +
49
+ '<defs>' +
50
+ '<linearGradient id="g1" x1="0%" y1="0%" x2="100%" y2="100%">' +
51
+ '<stop offset="0%" stop-color="' + stops[0] + '"/>' +
52
+ '<stop offset="50%" stop-color="' + stops[1] + '"/>' +
53
+ '<stop offset="100%" stop-color="' + stops[2] + '"/>' +
54
+ '</linearGradient>' +
55
+ '<filter id="f1">' +
56
+ '<feTurbulence type="fractalNoise" baseFrequency="' + f.freq + '" numOctaves="' + f.oct + '" seed="' + f.seed + '"/>' +
57
+ '<feDisplacementMap in="SourceGraphic" scale="' + f.scale + '"/>' +
58
+ '</filter>' +
59
+ '</defs>' +
60
+ '<rect width="100%" height="100%" fill="url(#g1)"/>' +
61
+ '<rect width="100%" height="100%" fill="url(#g1)" filter="url(#f1)" opacity="' + f.opacity + '"/>' +
62
+ '</svg>';
63
+ }
64
+
65
+ function patternSVG(stops, sz) {
66
+ var s = sz || 60;
67
+ var r1 = Math.round(s * 0.13);
68
+ var r2 = Math.round(s * 0.08);
69
+ return '<svg xmlns="http://www.w3.org/2000/svg" width="' + s + '" height="' + s + '" viewBox="0 0 ' + s + ' ' + s + '">' +
70
+ '<circle cx="' + (s/2) + '" cy="' + (s/2) + '" r="' + r1 + '" fill="none" stroke="' + stops[1] + '" stroke-width="0.5" opacity="0.3"/>' +
71
+ '<circle cx="0" cy="0" r="' + r2 + '" fill="none" stroke="' + stops[2] + '" stroke-width="0.5" opacity="0.2"/>' +
72
+ '<circle cx="' + s + '" cy="' + s + '" r="' + r2 + '" fill="none" stroke="' + stops[2] + '" stroke-width="0.5" opacity="0.2"/>' +
73
+ '<circle cx="' + s + '" cy="0" r="' + r2 + '" fill="none" stroke="' + stops[2] + '" stroke-width="0.5" opacity="0.2"/>' +
74
+ '<circle cx="0" cy="' + s + '" r="' + r2 + '" fill="none" stroke="' + stops[2] + '" stroke-width="0.5" opacity="0.2"/>' +
75
+ '</svg>';
76
+ }
77
+
78
+ function svgToUri(svg) {
79
+ return 'data:image/svg+xml,' + encodeURIComponent(svg);
80
+ }
81
+
82
+ /* ===================================================================
83
+ * Palette generation (requires chroma.js)
84
+ * =================================================================== */
85
+ function hasChroma() { return typeof chroma !== 'undefined'; }
86
+
87
+ function tints(hex, n) {
88
+ if (!hasChroma()) return [];
89
+ var out = [];
90
+ for (var i = 0; i < n; i++) {
91
+ out.push(chroma.mix(hex, '#ffffff', (i + 1) / (n + 1), 'lab').hex());
92
+ }
93
+ return out;
94
+ }
95
+
96
+ function surfaceColors(darkHex) {
97
+ if (!hasChroma()) return [];
98
+ var base = chroma(darkHex).luminance() < 0.15 ? darkHex : chroma(darkHex).darken(3).hex();
99
+ return chroma.scale([base, chroma(base).brighten(3).hex()]).mode('lab').colors(6);
100
+ }
101
+
102
+ function tonalSurface(darkHex, accent) {
103
+ if (!hasChroma()) return [];
104
+ var base = chroma(darkHex).luminance() < 0.15 ? darkHex : chroma(darkHex).darken(3).hex();
105
+ var tinted = chroma.mix(base, accent, 0.15, 'lab').hex();
106
+ var tintedLight = chroma(chroma.mix(base, accent, 0.4, 'lab')).brighten(2).hex();
107
+ return chroma.scale([tinted, tintedLight]).mode('lab').colors(6);
108
+ }
109
+
110
+ function semanticPalette(refHex) {
111
+ if (!hasChroma()) return { success: [], warning: [], danger: [], info: [] };
112
+ var h = chroma(refHex).get('hsl.h') || 0;
113
+ return {
114
+ success: [
115
+ chroma.hsl((h + 150) % 360, 0.6, 0.35).hex(),
116
+ chroma.hsl((h + 150) % 360, 0.65, 0.55).hex(),
117
+ chroma.hsl((h + 150) % 360, 0.5, 0.75).hex()
118
+ ],
119
+ warning: [
120
+ chroma.hsl(40, 0.6, 0.4).hex(),
121
+ chroma.hsl(40, 0.65, 0.6).hex(),
122
+ chroma.hsl(40, 0.5, 0.8).hex()
123
+ ],
124
+ danger: [
125
+ chroma.hsl(0, 0.65, 0.36).hex(),
126
+ chroma.hsl(0, 0.65, 0.56).hex(),
127
+ chroma.hsl(0, 0.5, 0.77).hex()
128
+ ],
129
+ info: [
130
+ chroma.hsl(215, 0.6, 0.33).hex(),
131
+ chroma.hsl(215, 0.65, 0.53).hex(),
132
+ chroma.hsl(215, 0.5, 0.73).hex()
133
+ ]
134
+ };
135
+ }
136
+
137
+ function wcagBadge(hex) {
138
+ if (!hasChroma()) return { label: '', cls: 'secondary', ratio: '?' };
139
+ var cW = chroma.contrast(hex, '#ffffff');
140
+ var cB = chroma.contrast(hex, '#000000');
141
+ var best = Math.max(cW, cB);
142
+ var label = best >= 7 ? 'AAA' : best >= 4.5 ? 'AA' : '';
143
+ var cls = best >= 7 ? 'success' : best >= 4.5 ? 'warning' : 'secondary';
144
+ return { label: label, cls: cls, ratio: best.toFixed(1) };
145
+ }
146
+
147
+ /* ===================================================================
148
+ * Custom skin persistence (localStorage)
149
+ * =================================================================== */
150
+ function loadCustom() {
151
+ try { state.customSkins = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); }
152
+ catch (e) { state.customSkins = {}; }
153
+ }
154
+
155
+ function saveCustom(name) {
156
+ state.customSkins[name] = {
157
+ stops: state.stops.slice(),
158
+ filter: { freq: state.filter.freq, oct: state.filter.oct, seed: state.filter.seed, scale: state.filter.scale, opacity: state.filter.opacity },
159
+ patternSize: state.patternSize
160
+ };
161
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state.customSkins));
162
+ }
163
+
164
+ function deleteCustom(name) {
165
+ delete state.customSkins[name];
166
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state.customSkins));
167
+ }
168
+
169
+ /* ===================================================================
170
+ * Live application to page
171
+ * =================================================================== */
172
+ function applyLive() {
173
+ var html = document.documentElement;
174
+ var gSvg = gradientSVG(state.stops, state.filter);
175
+ var pSvg = patternSVG(state.stops, state.patternSize);
176
+ html.style.setProperty('--zer0-bg-gradient', 'url("' + svgToUri(gSvg) + '")');
177
+ html.style.setProperty('--zer0-bg-pattern', 'url("' + svgToUri(pSvg) + '")');
178
+ html.style.setProperty('--zer0-bg-pattern-size', state.patternSize + 'px ' + state.patternSize + 'px');
179
+ toast('Skin applied live!');
180
+ }
181
+
182
+ function resetLive(skinName) {
183
+ var html = document.documentElement;
184
+ html.style.removeProperty('--zer0-bg-gradient');
185
+ html.style.removeProperty('--zer0-bg-pattern');
186
+ html.style.removeProperty('--zer0-bg-pattern-size');
187
+ if (typeof zer0Bg !== 'undefined') zer0Bg.setSkin(skinName || state.baseSkin);
188
+ toast('Reset to ' + (skinName || state.baseSkin));
189
+ }
190
+
191
+ /* ===================================================================
192
+ * UI helpers
193
+ * =================================================================== */
194
+ function toast(msg) {
195
+ var el = document.getElementById('skin-editor-toast');
196
+ if (!el) {
197
+ el = document.createElement('div');
198
+ el.id = 'skin-editor-toast';
199
+ el.className = 'position-fixed bottom-0 end-0 m-3 p-2 px-3 rounded bg-success text-white small shadow';
200
+ el.style.cssText = 'z-index:9999;transition:opacity .3s;opacity:0;pointer-events:none';
201
+ document.body.appendChild(el);
202
+ }
203
+ el.textContent = msg;
204
+ el.style.opacity = '1';
205
+ setTimeout(function () { el.style.opacity = '0'; }, 2000);
206
+ }
207
+
208
+ function swatchHTML(hex, label) {
209
+ var w = wcagBadge(hex);
210
+ var badge = w.label ? '<span class="badge bg-' + w.cls + '" style="font-size:.55rem">' + w.label + ' ' + w.ratio + '</span>' : '';
211
+ return '<div class="text-center" style="min-width:56px;cursor:pointer" title="Click to copy ' + hex + '" onclick="navigator.clipboard.writeText(\'' + hex + '\')">' +
212
+ '<div style="width:52px;height:36px;background:' + hex + ';border-radius:6px;border:1px solid rgba(128,128,128,.2)"></div>' +
213
+ '<div class="font-monospace mt-1" style="font-size:.65rem">' + hex + '</div>' +
214
+ (label ? '<div class="text-body-secondary" style="font-size:.6rem">' + label + '</div>' : '') +
215
+ badge +
216
+ '</div>';
217
+ }
218
+
219
+ function swatchRow(colors, labels) {
220
+ var html = '<div class="d-flex flex-wrap gap-2">';
221
+ for (var i = 0; i < colors.length; i++) {
222
+ html += swatchHTML(colors[i], labels ? labels[i] : null);
223
+ }
224
+ html += '</div>';
225
+ return html;
226
+ }
227
+
228
+ /* ===================================================================
229
+ * Render functions
230
+ * =================================================================== */
231
+ function populateSelect() {
232
+ var sel = document.getElementById('skin-editor-select');
233
+ if (!sel) return;
234
+ sel.innerHTML = '';
235
+
236
+ // Built-in group
237
+ var optBuiltin = document.createElement('optgroup');
238
+ optBuiltin.label = 'Built-in Skins';
239
+ var names = Object.keys(BUILTIN_SKINS);
240
+ for (var i = 0; i < names.length; i++) {
241
+ var opt = document.createElement('option');
242
+ opt.value = names[i];
243
+ opt.textContent = names[i].charAt(0).toUpperCase() + names[i].slice(1);
244
+ if (names[i] === state.baseSkin && !state.isCustom) opt.selected = true;
245
+ optBuiltin.appendChild(opt);
246
+ }
247
+ sel.appendChild(optBuiltin);
248
+
249
+ // Custom group
250
+ var customNames = Object.keys(state.customSkins);
251
+ if (customNames.length > 0) {
252
+ var optCustom = document.createElement('optgroup');
253
+ optCustom.label = 'Custom Skins';
254
+ for (var j = 0; j < customNames.length; j++) {
255
+ var opt2 = document.createElement('option');
256
+ opt2.value = 'custom:' + customNames[j];
257
+ opt2.textContent = '\u2B50 ' + customNames[j];
258
+ if (customNames[j] === state.baseSkin && state.isCustom) opt2.selected = true;
259
+ optCustom.appendChild(opt2);
260
+ }
261
+ sel.appendChild(optCustom);
262
+ }
263
+ }
264
+
265
+ function renderStops() {
266
+ var container = document.getElementById('skin-editor-stops');
267
+ if (!container) return;
268
+ container.innerHTML = '';
269
+
270
+ for (var i = 0; i < 3; i++) {
271
+ (function (idx) {
272
+ var color = state.stops[idx];
273
+ var col = document.createElement('div');
274
+ col.className = 'col-12 col-md-4';
275
+ col.innerHTML =
276
+ '<div class="card border-0 shadow-sm h-100">' +
277
+ '<div class="card-body p-3">' +
278
+ '<div class="d-flex align-items-center justify-content-between mb-2">' +
279
+ '<span class="fw-semibold small">' + STOP_LABELS[idx] + '</span>' +
280
+ '<span class="font-monospace small text-body-secondary" id="stop-hex-' + idx + '">' + color + '</span>' +
281
+ '</div>' +
282
+ '<div style="height:56px;border-radius:8px;background:' + color + ';border:1px solid rgba(128,128,128,.15);margin-bottom:8px" id="stop-preview-' + idx + '"></div>' +
283
+ '<div class="input-group input-group-sm">' +
284
+ '<input type="color" class="form-control form-control-color" value="' + color + '" id="stop-color-' + idx + '" style="min-width:40px">' +
285
+ '<input type="text" class="form-control font-monospace" value="' + color + '" id="stop-text-' + idx + '" maxlength="7">' +
286
+ '</div>' +
287
+ '</div>' +
288
+ '</div>';
289
+ container.appendChild(col);
290
+
291
+ var colorInput = document.getElementById('stop-color-' + idx);
292
+ var textInput = document.getElementById('stop-text-' + idx);
293
+
294
+ colorInput.addEventListener('input', function () {
295
+ state.stops[idx] = this.value;
296
+ textInput.value = this.value;
297
+ document.getElementById('stop-hex-' + idx).textContent = this.value;
298
+ document.getElementById('stop-preview-' + idx).style.background = this.value;
299
+ renderPreview();
300
+ renderPalettes();
301
+ });
302
+
303
+ textInput.addEventListener('change', function () {
304
+ var v = this.value.charAt(0) === '#' ? this.value : '#' + this.value;
305
+ if (/^#[0-9a-fA-F]{6}$/.test(v)) {
306
+ state.stops[idx] = v;
307
+ colorInput.value = v;
308
+ document.getElementById('stop-hex-' + idx).textContent = v;
309
+ document.getElementById('stop-preview-' + idx).style.background = v;
310
+ renderPreview();
311
+ renderPalettes();
312
+ }
313
+ });
314
+ })(i);
315
+ }
316
+ }
317
+
318
+ function renderPreview() {
319
+ var container = document.getElementById('skin-editor-preview');
320
+ if (!container) return;
321
+ var svg = gradientSVG(state.stops, state.filter);
322
+ container.style.background = 'url("' + svgToUri(svg) + '") center/cover no-repeat';
323
+ }
324
+
325
+ function renderPalettes() {
326
+ var container = document.getElementById('skin-editor-palettes');
327
+ if (!container) return;
328
+ if (!hasChroma()) {
329
+ container.innerHTML = '<div class="alert alert-warning small"><i class="bi bi-exclamation-triangle me-1"></i>chroma.js not loaded — palette generation unavailable.</div>';
330
+ return;
331
+ }
332
+
333
+ var html = '';
334
+
335
+ // ─── Gradient scale ───
336
+ var scaleColors = chroma.scale(state.stops).mode('lab').colors(11);
337
+ html += '<h6 class="fw-semibold mt-2"><i class="bi bi-rainbow me-1"></i>Gradient Scale</h6>';
338
+ html += '<div class="d-flex mb-3" style="height:32px;border-radius:8px;overflow:hidden;border:1px solid rgba(128,128,128,.15)">';
339
+ for (var s = 0; s < scaleColors.length; s++) {
340
+ html += '<div style="flex:1;background:' + scaleColors[s] + ';cursor:pointer" title="' + scaleColors[s] + '" onclick="navigator.clipboard.writeText(\'' + scaleColors[s] + '\')"></div>';
341
+ }
342
+ html += '</div>';
343
+
344
+ // ─── Primary tints per stop ───
345
+ html += '<h6 class="fw-semibold"><i class="bi bi-droplet-half me-1"></i>Primary Palette</h6>';
346
+ html += '<p class="text-body-secondary small mb-2">Tints from each gradient stop — use for buttons, links, and badges.</p>';
347
+ var aLabels = ['base', 'a10', 'a20', 'a30', 'a40', 'a50', 'a60'];
348
+ for (var p = 0; p < 3; p++) {
349
+ var stopTints = tints(state.stops[p], 6);
350
+ html += '<div class="mb-3"><span class="small fw-semibold text-body-secondary">' + STOP_LABELS[p] + ' — <span class="font-monospace">' + state.stops[p] + '</span></span>';
351
+ html += swatchRow([state.stops[p]].concat(stopTints), aLabels);
352
+ html += '</div>';
353
+ }
354
+
355
+ // ─── Surface palette ───
356
+ html += '<h6 class="fw-semibold mt-4"><i class="bi bi-layers me-1"></i>Surface Palette</h6>';
357
+ html += '<p class="text-body-secondary small mb-2">Surface colors for cards, backgrounds, and panels.</p>';
358
+ var darkest = state.stops.reduce(function (a, b) { return chroma(a).luminance() < chroma(b).luminance() ? a : b; });
359
+ var surfaces = surfaceColors(darkest);
360
+ html += swatchRow(surfaces, ['s10', 's20', 's30', 's40', 's50', 's60']);
361
+
362
+ // ─── Tonal surface ───
363
+ html += '<h6 class="fw-semibold mt-4"><i class="bi bi-vinyl me-1"></i>Tonal Surface</h6>';
364
+ html += '<p class="text-body-secondary small mb-2">Surface tinted with the accent color.</p>';
365
+ var tonal = tonalSurface(darkest, state.stops[1]);
366
+ html += swatchRow(tonal, ['t10', 't20', 't30', 't40', 't50', 't60']);
367
+
368
+ // ─── Semantic colors ───
369
+ html += '<h6 class="fw-semibold mt-4"><i class="bi bi-check-circle me-1"></i>Semantic Colors</h6>';
370
+ html += '<p class="text-body-secondary small mb-2">Auto-derived success, warning, danger, and info shades.</p>';
371
+ var sem = semanticPalette(state.stops[0]);
372
+ var semLabels = ['a10', 'a20', 'a30'];
373
+ var semTypes = [
374
+ { key: 'success', icon: 'check-circle-fill', color: '#198754' },
375
+ { key: 'warning', icon: 'exclamation-triangle-fill', color: '#ffc107' },
376
+ { key: 'danger', icon: 'x-circle-fill', color: '#dc3545' },
377
+ { key: 'info', icon: 'info-circle-fill', color: '#0dcaf0' }
378
+ ];
379
+ html += '<div class="row g-3">';
380
+ for (var t = 0; t < semTypes.length; t++) {
381
+ var st = semTypes[t];
382
+ html += '<div class="col-6">';
383
+ html += '<div class="d-flex align-items-center gap-1 mb-1"><i class="bi bi-' + st.icon + '" style="color:' + st.color + '"></i><span class="small fw-semibold">' + st.key.charAt(0).toUpperCase() + st.key.slice(1) + '</span></div>';
384
+ html += swatchRow(sem[st.key], semLabels);
385
+ html += '</div>';
386
+ }
387
+ html += '</div>';
388
+
389
+ container.innerHTML = html;
390
+ }
391
+
392
+ function renderFilters() {
393
+ var container = document.getElementById('skin-editor-filters');
394
+ if (!container) return;
395
+
396
+ var filters = [
397
+ { key: 'freq', label: 'Base Frequency', min: 0.001, max: 0.02, step: 0.001, val: state.filter.freq },
398
+ { key: 'oct', label: 'Octaves', min: 1, max: 8, step: 1, val: state.filter.oct },
399
+ { key: 'seed', label: 'Noise Seed', min: 0, max: 100, step: 1, val: state.filter.seed },
400
+ { key: 'scale', label: 'Displacement Scale', min: 0, max: 150, step: 5, val: state.filter.scale },
401
+ { key: 'opacity', label: 'Overlay Opacity', min: 0, max: 1, step: 0.05, val: state.filter.opacity }
402
+ ];
403
+
404
+ var html = '<div class="row g-3">';
405
+ for (var i = 0; i < filters.length; i++) {
406
+ var f = filters[i];
407
+ html += '<div class="col-12 col-md-6">' +
408
+ '<label class="form-label small fw-semibold d-flex justify-content-between">' + f.label +
409
+ ' <span class="text-body-secondary font-monospace" id="filter-val-' + f.key + '">' + f.val + '</span></label>' +
410
+ '<input type="range" class="form-range" id="filter-' + f.key + '" min="' + f.min + '" max="' + f.max + '" step="' + f.step + '" value="' + f.val + '">' +
411
+ '</div>';
412
+ }
413
+ // Pattern size
414
+ html += '<div class="col-12 col-md-6">' +
415
+ '<label class="form-label small fw-semibold d-flex justify-content-between">Pattern Tile Size' +
416
+ ' <span class="text-body-secondary font-monospace" id="filter-val-patternSize">' + state.patternSize + 'px</span></label>' +
417
+ '<input type="range" class="form-range" id="filter-patternSize" min="20" max="100" step="2" value="' + state.patternSize + '">' +
418
+ '</div>';
419
+ html += '</div>';
420
+
421
+ container.innerHTML = html;
422
+
423
+ // Bind filter slider events
424
+ for (var j = 0; j < filters.length; j++) {
425
+ (function (f) {
426
+ var input = document.getElementById('filter-' + f.key);
427
+ if (!input) return;
428
+ input.addEventListener('input', function () {
429
+ state.filter[f.key] = parseFloat(this.value);
430
+ document.getElementById('filter-val-' + f.key).textContent = this.value;
431
+ renderPreview();
432
+ });
433
+ })(filters[j]);
434
+ }
435
+
436
+ var pInput = document.getElementById('filter-patternSize');
437
+ if (pInput) {
438
+ pInput.addEventListener('input', function () {
439
+ state.patternSize = parseInt(this.value, 10);
440
+ document.getElementById('filter-val-patternSize').textContent = this.value + 'px';
441
+ });
442
+ }
443
+ }
444
+
445
+ /* ===================================================================
446
+ * Load a skin into the editor
447
+ * =================================================================== */
448
+ function loadSkin(name, isCustom) {
449
+ var def = isCustom ? state.customSkins[name] : BUILTIN_SKINS[name];
450
+ if (!def) return;
451
+ state.baseSkin = name;
452
+ state.isCustom = !!isCustom;
453
+ state.stops = def.stops.slice();
454
+ state.filter = { freq: def.filter.freq, oct: def.filter.oct, seed: def.filter.seed, scale: def.filter.scale, opacity: def.filter.opacity };
455
+ state.patternSize = def.patternSize;
456
+ renderStops();
457
+ renderPreview();
458
+ renderPalettes();
459
+ renderFilters();
460
+ }
461
+
462
+ /* ===================================================================
463
+ * Export helpers
464
+ * =================================================================== */
465
+ function downloadFile(filename, content, type) {
466
+ var blob = new Blob([content], { type: type });
467
+ var url = URL.createObjectURL(blob);
468
+ var a = document.createElement('a');
469
+ a.href = url;
470
+ a.download = filename;
471
+ document.body.appendChild(a);
472
+ a.click();
473
+ document.body.removeChild(a);
474
+ URL.revokeObjectURL(url);
475
+ }
476
+
477
+ function exportSVGs() {
478
+ var name = state.baseSkin || 'custom';
479
+ downloadFile(name + '-gradient.svg', gradientSVG(state.stops, state.filter), 'image/svg+xml');
480
+ setTimeout(function () {
481
+ downloadFile(name + '-pattern.svg', patternSVG(state.stops, state.patternSize), 'image/svg+xml');
482
+ }, 300);
483
+ toast('SVG files downloaded');
484
+ }
485
+
486
+ function exportCSS() {
487
+ var gUri = svgToUri(gradientSVG(state.stops, state.filter));
488
+ var pUri = svgToUri(patternSVG(state.stops, state.patternSize));
489
+ var css = '/* Custom skin: ' + state.baseSkin + ' */\n' +
490
+ '[data-theme-skin="' + state.baseSkin + '"] {\n' +
491
+ ' --zer0-bg-gradient: url("' + gUri + '");\n' +
492
+ ' --zer0-bg-pattern: url("' + pUri + '");\n' +
493
+ ' --zer0-bg-pattern-size: ' + state.patternSize + 'px ' + state.patternSize + 'px;\n' +
494
+ '}\n';
495
+ navigator.clipboard.writeText(css).then(function () { toast('CSS copied to clipboard'); });
496
+ }
497
+
498
+ /* ===================================================================
499
+ * Event handlers
500
+ * =================================================================== */
501
+ function setupEvents() {
502
+ // Select change → load skin
503
+ var sel = document.getElementById('skin-editor-select');
504
+ if (sel) sel.addEventListener('change', function () {
505
+ var val = this.value;
506
+ if (val.indexOf('custom:') === 0) {
507
+ loadSkin(val.slice(7), true);
508
+ } else {
509
+ loadSkin(val, false);
510
+ }
511
+ });
512
+
513
+ // Save
514
+ var saveBtn = document.getElementById('skin-editor-save');
515
+ if (saveBtn) saveBtn.addEventListener('click', function () {
516
+ var name = prompt('Name this custom skin:', state.baseSkin + '-custom');
517
+ if (name && name.trim()) {
518
+ var clean = name.trim().toLowerCase().replace(/[^a-z0-9_-]/g, '-');
519
+ saveCustom(clean);
520
+ populateSelect();
521
+ // Select the newly saved one
522
+ var newSel = document.getElementById('skin-editor-select');
523
+ if (newSel) newSel.value = 'custom:' + clean;
524
+ state.baseSkin = clean;
525
+ state.isCustom = true;
526
+ toast('Saved "' + clean + '"');
527
+ }
528
+ });
529
+
530
+ // Delete
531
+ var deleteBtn = document.getElementById('skin-editor-delete');
532
+ if (deleteBtn) deleteBtn.addEventListener('click', function () {
533
+ var selVal = document.getElementById('skin-editor-select');
534
+ if (!selVal) return;
535
+ var val = selVal.value;
536
+ if (val.indexOf('custom:') === 0) {
537
+ var cName = val.slice(7);
538
+ if (confirm('Delete custom skin "' + cName + '"?')) {
539
+ deleteCustom(cName);
540
+ loadSkin('aqua', false);
541
+ populateSelect();
542
+ toast('Deleted "' + cName + '"');
543
+ }
544
+ } else {
545
+ toast('Cannot delete built-in skins');
546
+ }
547
+ });
548
+
549
+ // Random
550
+ var randBtn = document.getElementById('skin-editor-random');
551
+ if (randBtn) randBtn.addEventListener('click', function () {
552
+ if (hasChroma()) {
553
+ var baseHue = Math.random() * 360;
554
+ state.stops = [
555
+ chroma.hsl(baseHue, 0.65 + Math.random() * 0.2, 0.3 + Math.random() * 0.15).hex(),
556
+ chroma.hsl((baseHue + 25 + Math.random() * 20) % 360, 0.55 + Math.random() * 0.2, 0.45 + Math.random() * 0.15).hex(),
557
+ chroma.hsl((baseHue + 50 + Math.random() * 30) % 360, 0.4 + Math.random() * 0.2, 0.65 + Math.random() * 0.15).hex()
558
+ ];
559
+ state.filter.seed = Math.floor(Math.random() * 100);
560
+ } else {
561
+ state.stops = [
562
+ '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0'),
563
+ '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0'),
564
+ '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0')
565
+ ];
566
+ }
567
+ renderStops();
568
+ renderPreview();
569
+ renderPalettes();
570
+ renderFilters();
571
+ });
572
+
573
+ // Reset to built-in
574
+ var resetBtn = document.getElementById('skin-editor-reset');
575
+ if (resetBtn) resetBtn.addEventListener('click', function () {
576
+ var original = state.isCustom ? 'aqua' : state.baseSkin;
577
+ loadSkin(original, false);
578
+ resetLive(original);
579
+ populateSelect();
580
+ });
581
+
582
+ // Apply live
583
+ var applyBtn = document.getElementById('skin-editor-apply');
584
+ if (applyBtn) applyBtn.addEventListener('click', applyLive);
585
+
586
+ // Export SVGs
587
+ var exportSvgBtn = document.getElementById('skin-editor-export-svg');
588
+ if (exportSvgBtn) exportSvgBtn.addEventListener('click', exportSVGs);
589
+
590
+ // Export CSS
591
+ var exportCssBtn = document.getElementById('skin-editor-export-css');
592
+ if (exportCssBtn) exportCssBtn.addEventListener('click', exportCSS);
593
+ }
594
+
595
+ /* ===================================================================
596
+ * Initialization
597
+ * =================================================================== */
598
+ function init() {
599
+ if (!document.getElementById('pane-skin-editor')) return;
600
+
601
+ loadCustom();
602
+
603
+ // Start with whatever skin is currently active
604
+ var current = (typeof zer0Bg !== 'undefined') ? zer0Bg.currentSkin() : 'aqua';
605
+ if (BUILTIN_SKINS[current]) {
606
+ state.baseSkin = current;
607
+ state.stops = BUILTIN_SKINS[current].stops.slice();
608
+ state.filter = { freq: BUILTIN_SKINS[current].filter.freq, oct: BUILTIN_SKINS[current].filter.oct, seed: BUILTIN_SKINS[current].filter.seed, scale: BUILTIN_SKINS[current].filter.scale, opacity: BUILTIN_SKINS[current].filter.opacity };
609
+ state.patternSize = BUILTIN_SKINS[current].patternSize;
610
+ }
611
+
612
+ populateSelect();
613
+ renderStops();
614
+ renderPreview();
615
+ renderPalettes();
616
+ renderFilters();
617
+ setupEvents();
618
+
619
+ console.log('[skin-editor] Initialized with skin:', state.baseSkin);
620
+ }
621
+
622
+ if (document.readyState === 'loading') {
623
+ document.addEventListener('DOMContentLoaded', init);
624
+ } else {
625
+ init();
626
+ }
627
+
628
+ // Re-sync when skin changes from outside (quick skin bar, etc.)
629
+ document.addEventListener('zer0:skin-change', function (e) {
630
+ var name = e.detail && e.detail.skin;
631
+ if (name && BUILTIN_SKINS[name] && name !== state.baseSkin) {
632
+ loadSkin(name, false);
633
+ populateSelect();
634
+ }
635
+ });
636
+
637
+ // Expose API for integration with other modules
638
+ window.skinEditor = {
639
+ applyLive: applyLive,
640
+ resetLive: resetLive,
641
+ getState: function () { return JSON.parse(JSON.stringify(state)); },
642
+ BUILTIN_SKINS: BUILTIN_SKINS
643
+ };
644
+
645
+ })();