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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +236 -0
- data/README.md +66 -19
- data/_data/navigation/admin.yml +53 -0
- data/_data/theme_backgrounds.yml +121 -0
- data/_includes/components/admin-tabs.html +59 -0
- data/_includes/components/analytics-dashboard.html +232 -0
- data/_includes/components/background-customizer.html +159 -0
- data/_includes/components/background-settings.html +137 -0
- data/_includes/components/collection-manager.html +151 -0
- data/_includes/components/component-showcase.html +452 -0
- data/_includes/components/config-editor.html +207 -0
- data/_includes/components/config-viewer.html +479 -0
- data/_includes/components/env-dashboard.html +154 -0
- data/_includes/components/feature-card.html +94 -0
- data/_includes/components/info-section.html +172 -149
- data/_includes/components/js-cdn.html +4 -1
- data/_includes/components/nav-editor.html +99 -0
- data/_includes/components/setup-banner.html +28 -0
- data/_includes/components/setup-check.html +53 -0
- data/_includes/components/svg-background.html +42 -0
- data/_includes/components/theme-customizer.html +46 -0
- data/_includes/content/seo.html +68 -135
- data/_includes/core/footer.html +1 -1
- data/_includes/core/head.html +3 -2
- data/_includes/core/header.html +14 -7
- data/_includes/landing/landing-install-cards.html +18 -7
- data/_includes/navigation/admin-nav.html +95 -0
- data/_includes/navigation/navbar.html +43 -5
- data/_includes/navigation/sidebar-left.html +1 -1
- data/_includes/setup/wizard.html +330 -0
- data/_layouts/admin.html +166 -0
- data/_layouts/landing.html +23 -9
- data/_layouts/root.html +12 -6
- data/_layouts/setup.html +73 -0
- data/_plugins/preview_image_generator.rb +26 -12
- data/_sass/core/_navbar.scss +2 -2
- data/_sass/custom.scss +28 -6
- data/_sass/theme/_background-mixins.scss +95 -0
- data/_sass/theme/_backgrounds.scss +156 -0
- data/_sass/theme/_color-modes.scss +2 -1
- data/assets/backgrounds/gradients/air.svg +15 -0
- data/assets/backgrounds/gradients/aqua.svg +15 -0
- data/assets/backgrounds/gradients/contrast.svg +15 -0
- data/assets/backgrounds/gradients/dark.svg +15 -0
- data/assets/backgrounds/gradients/dirt.svg +15 -0
- data/assets/backgrounds/gradients/mint.svg +15 -0
- data/assets/backgrounds/gradients/neon.svg +15 -0
- data/assets/backgrounds/gradients/plum.svg +15 -0
- data/assets/backgrounds/gradients/sunrise.svg +15 -0
- data/assets/backgrounds/noise/air.svg +8 -0
- data/assets/backgrounds/noise/aqua.svg +8 -0
- data/assets/backgrounds/noise/contrast.svg +8 -0
- data/assets/backgrounds/noise/dark.svg +8 -0
- data/assets/backgrounds/noise/dirt.svg +8 -0
- data/assets/backgrounds/noise/mint.svg +8 -0
- data/assets/backgrounds/noise/neon.svg +8 -0
- data/assets/backgrounds/noise/plum.svg +8 -0
- data/assets/backgrounds/noise/sunrise.svg +8 -0
- data/assets/backgrounds/patterns/air.svg +7 -0
- data/assets/backgrounds/patterns/aqua.svg +7 -0
- data/assets/backgrounds/patterns/contrast.svg +4 -0
- data/assets/backgrounds/patterns/dark.svg +5 -0
- data/assets/backgrounds/patterns/dirt.svg +5 -0
- data/assets/backgrounds/patterns/mint.svg +6 -0
- data/assets/backgrounds/patterns/neon.svg +6 -0
- data/assets/backgrounds/patterns/plum.svg +6 -0
- data/assets/backgrounds/patterns/sunrise.svg +5 -0
- data/assets/js/background-customizer.js +73 -0
- data/assets/js/code-copy.js +18 -47
- data/assets/js/config-utility.js +307 -0
- data/assets/js/nav-editor.js +39 -0
- data/assets/js/palette-generator.js +415 -0
- data/assets/js/search-modal.js +31 -11
- data/assets/js/setup-wizard.js +306 -0
- data/assets/js/skin-editor.js +645 -0
- data/assets/js/theme-customizer.js +102 -0
- data/assets/js/ui-enhancements.js +15 -24
- data/assets/vendor/bootstrap/css/bootstrap.min.css +1 -0
- data/assets/vendor/bootstrap/js/bootstrap.bundle.min.js +1 -0
- data/scripts/README.md +45 -0
- data/scripts/features/generate-preview-images +297 -7
- data/scripts/features/install-preview-generator +51 -33
- data/scripts/fork-cleanup.sh +92 -19
- data/scripts/github-setup.sh +284 -0
- data/scripts/init_setup.sh +0 -1
- data/scripts/lib/frontmatter.sh +543 -0
- data/scripts/lib/migrate.sh +265 -0
- data/scripts/lib/preview_generator.py +607 -32
- data/scripts/lint-pages +505 -0
- data/scripts/migrate.sh +201 -0
- data/scripts/platform/setup-linux.sh +244 -0
- data/scripts/platform/setup-macos.sh +187 -0
- data/scripts/platform/setup-wsl.sh +196 -0
- metadata +71 -6
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* palette-generator.js
|
|
3
|
+
* Color palette generator + live CSS variable editor for the Theme Customizer.
|
|
4
|
+
*
|
|
5
|
+
* Dependencies: chroma.js (loaded via CDN in head)
|
|
6
|
+
*
|
|
7
|
+
* Provides:
|
|
8
|
+
* - Palette generation from a base color (complementary, analogous, triadic, etc.)
|
|
9
|
+
* - Live preview of Bootstrap CSS custom properties on document root
|
|
10
|
+
* - WCAG contrast ratio display
|
|
11
|
+
* - Export generated palette to YAML
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
/* ── Guard: chroma.js must be loaded ───────────────────────── */
|
|
18
|
+
if (typeof chroma === 'undefined') {
|
|
19
|
+
console.warn('[palette-generator] chroma.js not loaded');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* ── State ─────────────────────────────────────────────────── */
|
|
24
|
+
var state = {
|
|
25
|
+
baseColor: getComputedStyle(document.documentElement).getPropertyValue('--bs-primary').trim() || '#0d6efd',
|
|
26
|
+
harmony: 'complementary',
|
|
27
|
+
palette: [],
|
|
28
|
+
liveOverrides: {} // key → value map of active CSS overrides
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/* ── Palette Harmony Algorithms ────────────────────────────── */
|
|
32
|
+
var harmonies = {
|
|
33
|
+
complementary: function (base) {
|
|
34
|
+
var c = chroma(base);
|
|
35
|
+
return [c, c.set('hsl.h', '+180')];
|
|
36
|
+
},
|
|
37
|
+
analogous: function (base) {
|
|
38
|
+
var c = chroma(base);
|
|
39
|
+
return [c.set('hsl.h', '-30'), c, c.set('hsl.h', '+30')];
|
|
40
|
+
},
|
|
41
|
+
triadic: function (base) {
|
|
42
|
+
var c = chroma(base);
|
|
43
|
+
return [c, c.set('hsl.h', '+120'), c.set('hsl.h', '+240')];
|
|
44
|
+
},
|
|
45
|
+
'split-complementary': function (base) {
|
|
46
|
+
var c = chroma(base);
|
|
47
|
+
return [c, c.set('hsl.h', '+150'), c.set('hsl.h', '+210')];
|
|
48
|
+
},
|
|
49
|
+
tetradic: function (base) {
|
|
50
|
+
var c = chroma(base);
|
|
51
|
+
return [c, c.set('hsl.h', '+90'), c.set('hsl.h', '+180'), c.set('hsl.h', '+270')];
|
|
52
|
+
},
|
|
53
|
+
monochromatic: function (base) {
|
|
54
|
+
var c = chroma(base);
|
|
55
|
+
return [
|
|
56
|
+
c.brighten(1.5),
|
|
57
|
+
c.brighten(0.75),
|
|
58
|
+
c,
|
|
59
|
+
c.darken(0.75),
|
|
60
|
+
c.darken(1.5)
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function generatePalette(base, harmonyName) {
|
|
66
|
+
var fn = harmonies[harmonyName] || harmonies.complementary;
|
|
67
|
+
return fn(base).map(function (c) { return c.hex(); });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function contrastRatio(fg, bg) {
|
|
71
|
+
return chroma.contrast(fg, bg).toFixed(2);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function contrastLabel(ratio) {
|
|
75
|
+
if (ratio >= 7) return '<span class="badge bg-success">AAA</span>';
|
|
76
|
+
if (ratio >= 4.5) return '<span class="badge bg-success">AA</span>';
|
|
77
|
+
if (ratio >= 3) return '<span class="badge bg-warning text-dark">AA Large</span>';
|
|
78
|
+
return '<span class="badge bg-danger">Fail</span>';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* ── Scale Generator ───────────────────────────────────────── */
|
|
82
|
+
function generateScale(hex, steps) {
|
|
83
|
+
steps = steps || 9;
|
|
84
|
+
return chroma.scale(['white', hex, 'black']).mode('lab').colors(steps + 2).slice(1, -1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* ── Render Palette Grid ───────────────────────────────────── */
|
|
88
|
+
function renderPalette() {
|
|
89
|
+
var container = document.getElementById('palette-swatches');
|
|
90
|
+
if (!container) return;
|
|
91
|
+
|
|
92
|
+
state.palette = generatePalette(state.baseColor, state.harmony);
|
|
93
|
+
var isDark = document.documentElement.getAttribute('data-bs-theme') === 'dark';
|
|
94
|
+
var textColor = isDark ? '#ffffff' : '#000000';
|
|
95
|
+
|
|
96
|
+
var html = '<div class="row g-2 mb-3">';
|
|
97
|
+
state.palette.forEach(function (hex, i) {
|
|
98
|
+
var ratio = contrastRatio(textColor, hex);
|
|
99
|
+
html += '<div class="col">' +
|
|
100
|
+
'<div class="palette-swatch rounded-3 p-3 text-center position-relative" ' +
|
|
101
|
+
'style="background:' + hex + '; min-height:100px; cursor:pointer" ' +
|
|
102
|
+
'data-palette-color="' + hex + '" title="Click to copy">' +
|
|
103
|
+
'<code class="d-block fw-bold" style="color:' +
|
|
104
|
+
(chroma.contrast(hex, '#fff') > 3 ? '#fff' : '#000') + '">' + hex + '</code>' +
|
|
105
|
+
'<small class="d-block mt-1" style="color:' +
|
|
106
|
+
(chroma.contrast(hex, '#fff') > 3 ? 'rgba(255,255,255,.7)' : 'rgba(0,0,0,.6)') + '">' +
|
|
107
|
+
contrastLabel(ratio) + ' ' + ratio + ':1</small>' +
|
|
108
|
+
'</div></div>';
|
|
109
|
+
});
|
|
110
|
+
html += '</div>';
|
|
111
|
+
|
|
112
|
+
// Scale for base color
|
|
113
|
+
html += '<h6 class="text-body-secondary small text-uppercase fw-semibold mt-4 mb-2">' +
|
|
114
|
+
'<i class="bi bi-bar-chart-steps me-1"></i>Base Color Scale</h6>';
|
|
115
|
+
var scale = generateScale(state.baseColor);
|
|
116
|
+
html += '<div class="d-flex rounded-3 overflow-hidden" style="height:48px">';
|
|
117
|
+
scale.forEach(function (hex, i) {
|
|
118
|
+
var label = (i + 1) * 100;
|
|
119
|
+
html += '<div class="flex-fill position-relative" style="background:' + hex + '" ' +
|
|
120
|
+
'data-palette-color="' + hex + '" title="' + label + ': ' + hex + '">' +
|
|
121
|
+
'<small class="position-absolute bottom-0 start-50 translate-middle-x" style="font-size:.6rem;color:' +
|
|
122
|
+
(chroma.contrast(hex, '#fff') > 3 ? '#fff' : '#000') + '">' + label + '</small></div>';
|
|
123
|
+
});
|
|
124
|
+
html += '</div>';
|
|
125
|
+
|
|
126
|
+
container.innerHTML = html;
|
|
127
|
+
|
|
128
|
+
// Click-to-copy on swatches
|
|
129
|
+
container.querySelectorAll('[data-palette-color]').forEach(function (el) {
|
|
130
|
+
el.addEventListener('click', function () {
|
|
131
|
+
navigator.clipboard.writeText(this.dataset.paletteColor).then(function () {
|
|
132
|
+
showToast('Copied ' + el.dataset.paletteColor);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* ── Bind Palette Controls ─────────────────────────────────── */
|
|
139
|
+
var basePicker = document.getElementById('palette-base-color');
|
|
140
|
+
var baseText = document.getElementById('palette-base-text');
|
|
141
|
+
var harmonySelect = document.getElementById('palette-harmony');
|
|
142
|
+
|
|
143
|
+
if (basePicker) {
|
|
144
|
+
basePicker.value = state.baseColor;
|
|
145
|
+
basePicker.addEventListener('input', function () {
|
|
146
|
+
state.baseColor = this.value;
|
|
147
|
+
if (baseText) baseText.value = this.value;
|
|
148
|
+
renderPalette();
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (baseText) {
|
|
152
|
+
baseText.value = state.baseColor;
|
|
153
|
+
baseText.addEventListener('change', function () {
|
|
154
|
+
if (chroma.valid(this.value)) {
|
|
155
|
+
state.baseColor = chroma(this.value).hex();
|
|
156
|
+
if (basePicker) basePicker.value = state.baseColor;
|
|
157
|
+
renderPalette();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (harmonySelect) {
|
|
162
|
+
harmonySelect.addEventListener('change', function () {
|
|
163
|
+
state.harmony = this.value;
|
|
164
|
+
renderPalette();
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Random palette button
|
|
169
|
+
var randomBtn = document.getElementById('palette-random');
|
|
170
|
+
if (randomBtn) {
|
|
171
|
+
randomBtn.addEventListener('click', function () {
|
|
172
|
+
state.baseColor = chroma.random().hex();
|
|
173
|
+
if (basePicker) basePicker.value = state.baseColor;
|
|
174
|
+
if (baseText) baseText.value = state.baseColor;
|
|
175
|
+
renderPalette();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* ── Live Preview: CSS Variable Editor ─────────────────────── */
|
|
180
|
+
var liveVars = {
|
|
181
|
+
// Bootstrap semantic colors
|
|
182
|
+
'--bs-primary': { label: 'Primary', type: 'color', default: '#0d6efd' },
|
|
183
|
+
'--bs-secondary': { label: 'Secondary', type: 'color', default: '#6c757d' },
|
|
184
|
+
'--bs-success': { label: 'Success', type: 'color', default: '#198754' },
|
|
185
|
+
'--bs-info': { label: 'Info', type: 'color', default: '#0dcaf0' },
|
|
186
|
+
'--bs-warning': { label: 'Warning', type: 'color', default: '#ffc107' },
|
|
187
|
+
'--bs-danger': { label: 'Danger', type: 'color', default: '#dc3545' },
|
|
188
|
+
// Body
|
|
189
|
+
'--bs-body-bg': { label: 'Body Background', type: 'color', default: '#ffffff' },
|
|
190
|
+
'--bs-body-color': { label: 'Body Text', type: 'color', default: '#212529' },
|
|
191
|
+
'--bs-tertiary-bg': { label: 'Tertiary BG', type: 'color', default: '#f8f9fa' },
|
|
192
|
+
// Borders/Links
|
|
193
|
+
'--bs-border-color': { label: 'Border Color', type: 'color', default: '#dee2e6' },
|
|
194
|
+
'--bs-link-color': { label: 'Link Color', type: 'color', default: '#0d6efd' },
|
|
195
|
+
'--bs-link-hover-color': { label: 'Link Hover', type: 'color', default: '#0a58ca' },
|
|
196
|
+
// Sizing
|
|
197
|
+
'--bs-border-radius': { label: 'Border Radius', type: 'range', min: 0, max: 2, step: 0.05, unit: 'rem', default: '0.375rem' },
|
|
198
|
+
'--bs-border-width': { label: 'Border Width', type: 'range', min: 0, max: 5, step: 0.5, unit: 'px', default: '1px' },
|
|
199
|
+
// Font
|
|
200
|
+
'--bs-body-font-size': { label: 'Font Size', type: 'range', min: 0.75, max: 1.5, step: 0.05, unit: 'rem', default: '1rem' },
|
|
201
|
+
'--bs-body-font-weight': { label: 'Font Weight', type: 'range', min: 100, max: 900, step: 100, unit: '', default: '400' },
|
|
202
|
+
'--bs-body-line-height': { label: 'Line Height', type: 'range', min: 1, max: 2.5, step: 0.05, unit: '', default: '1.5' }
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
function readCurrentCSSVar(name) {
|
|
206
|
+
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function renderLiveEditor() {
|
|
210
|
+
var container = document.getElementById('live-editor-fields');
|
|
211
|
+
if (!container) return;
|
|
212
|
+
|
|
213
|
+
var html = '';
|
|
214
|
+
var categories = {
|
|
215
|
+
'Theme Colors': ['--bs-primary', '--bs-secondary', '--bs-success', '--bs-info', '--bs-warning', '--bs-danger'],
|
|
216
|
+
'Body & Layout': ['--bs-body-bg', '--bs-body-color', '--bs-tertiary-bg'],
|
|
217
|
+
'Links & Borders': ['--bs-border-color', '--bs-link-color', '--bs-link-hover-color'],
|
|
218
|
+
'Sizing & Typography': ['--bs-border-radius', '--bs-border-width', '--bs-body-font-size', '--bs-body-font-weight', '--bs-body-line-height']
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
Object.keys(categories).forEach(function (catName) {
|
|
222
|
+
html += '<h6 class="text-body-secondary small text-uppercase fw-semibold mt-3 mb-2">' +
|
|
223
|
+
'<i class="bi bi-sliders me-1"></i>' + catName + '</h6>';
|
|
224
|
+
html += '<div class="row g-2">';
|
|
225
|
+
|
|
226
|
+
categories[catName].forEach(function (varName) {
|
|
227
|
+
var cfg = liveVars[varName];
|
|
228
|
+
var current = readCurrentCSSVar(varName) || cfg.default;
|
|
229
|
+
|
|
230
|
+
if (cfg.type === 'color') {
|
|
231
|
+
// Normalize to hex
|
|
232
|
+
var hex;
|
|
233
|
+
try { hex = chroma(current).hex(); } catch (e) { hex = cfg.default; }
|
|
234
|
+
html += '<div class="col-6 col-md-4 col-lg-3">' +
|
|
235
|
+
'<label class="form-label small fw-semibold mb-1">' + cfg.label + '</label>' +
|
|
236
|
+
'<div class="input-group input-group-sm">' +
|
|
237
|
+
'<input type="color" class="form-control form-control-color" value="' + hex + '" data-live-var="' + varName + '">' +
|
|
238
|
+
'<input type="text" class="form-control font-monospace" value="' + hex + '" data-live-text="' + varName + '">' +
|
|
239
|
+
'</div></div>';
|
|
240
|
+
} else if (cfg.type === 'range') {
|
|
241
|
+
var numVal = parseFloat(current) || parseFloat(cfg.default);
|
|
242
|
+
html += '<div class="col-6 col-md-4 col-lg-3">' +
|
|
243
|
+
'<label class="form-label small fw-semibold mb-1">' + cfg.label + '</label>' +
|
|
244
|
+
'<div class="d-flex align-items-center gap-2">' +
|
|
245
|
+
'<input type="range" class="form-range flex-grow-1" min="' + cfg.min + '" max="' + cfg.max +
|
|
246
|
+
'" step="' + cfg.step + '" value="' + numVal + '" data-live-var="' + varName + '" data-unit="' + cfg.unit + '">' +
|
|
247
|
+
'<code class="text-nowrap" data-live-val="' + varName + '">' + numVal + cfg.unit + '</code>' +
|
|
248
|
+
'</div></div>';
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
html += '</div>';
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
container.innerHTML = html;
|
|
255
|
+
bindLiveEditorEvents(container);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function bindLiveEditorEvents(container) {
|
|
259
|
+
// Color pickers
|
|
260
|
+
container.querySelectorAll('[data-live-var][type="color"]').forEach(function (picker) {
|
|
261
|
+
picker.addEventListener('input', function () {
|
|
262
|
+
var varName = this.dataset.liveVar;
|
|
263
|
+
var cfg = liveVars[varName];
|
|
264
|
+
applyLiveVar(varName, this.value);
|
|
265
|
+
|
|
266
|
+
var textInput = container.querySelector('[data-live-text="' + varName + '"]');
|
|
267
|
+
if (textInput) textInput.value = this.value;
|
|
268
|
+
|
|
269
|
+
// Also set the rgb variant if it's a semantic color
|
|
270
|
+
if (varName.match(/^--bs-(primary|secondary|success|info|warning|danger)$/)) {
|
|
271
|
+
var rgb = chroma(this.value).rgb().join(', ');
|
|
272
|
+
applyLiveVar(varName + '-rgb', rgb);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Color text inputs
|
|
278
|
+
container.querySelectorAll('[data-live-text]').forEach(function (input) {
|
|
279
|
+
input.addEventListener('change', function () {
|
|
280
|
+
var varName = this.dataset.liveText;
|
|
281
|
+
if (chroma.valid(this.value)) {
|
|
282
|
+
var hex = chroma(this.value).hex();
|
|
283
|
+
this.value = hex;
|
|
284
|
+
applyLiveVar(varName, hex);
|
|
285
|
+
var picker = container.querySelector('[data-live-var="' + varName + '"][type="color"]');
|
|
286
|
+
if (picker) picker.value = hex;
|
|
287
|
+
|
|
288
|
+
if (varName.match(/^--bs-(primary|secondary|success|info|warning|danger)$/)) {
|
|
289
|
+
var rgb = chroma(hex).rgb().join(', ');
|
|
290
|
+
applyLiveVar(varName + '-rgb', rgb);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Range sliders
|
|
297
|
+
container.querySelectorAll('[data-live-var][type="range"]').forEach(function (slider) {
|
|
298
|
+
slider.addEventListener('input', function () {
|
|
299
|
+
var varName = this.dataset.liveVar;
|
|
300
|
+
var unit = this.dataset.unit || '';
|
|
301
|
+
var val = this.value + unit;
|
|
302
|
+
applyLiveVar(varName, val);
|
|
303
|
+
var display = container.querySelector('[data-live-val="' + varName + '"]');
|
|
304
|
+
if (display) display.textContent = val;
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function applyLiveVar(name, value) {
|
|
310
|
+
document.documentElement.style.setProperty(name, value);
|
|
311
|
+
state.liveOverrides[name] = value;
|
|
312
|
+
// Rebuild export YAML if the export function exists
|
|
313
|
+
if (typeof rebuildFullYaml === 'function') rebuildFullYaml();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/* ── Apply Palette to Live Preview ─────────────────────────── */
|
|
317
|
+
var applyPaletteBtn = document.getElementById('palette-apply');
|
|
318
|
+
if (applyPaletteBtn) {
|
|
319
|
+
applyPaletteBtn.addEventListener('click', function () {
|
|
320
|
+
if (state.palette.length < 2) return;
|
|
321
|
+
var mapping = ['--bs-primary', '--bs-secondary', '--bs-success', '--bs-info', '--bs-warning', '--bs-danger'];
|
|
322
|
+
|
|
323
|
+
state.palette.forEach(function (hex, i) {
|
|
324
|
+
if (i < mapping.length) {
|
|
325
|
+
applyLiveVar(mapping[i], hex);
|
|
326
|
+
var rgb = chroma(hex).rgb().join(', ');
|
|
327
|
+
applyLiveVar(mapping[i] + '-rgb', rgb);
|
|
328
|
+
|
|
329
|
+
// Update the live editor inputs if they exist
|
|
330
|
+
var picker = document.querySelector('[data-live-var="' + mapping[i] + '"][type="color"]');
|
|
331
|
+
var text = document.querySelector('[data-live-text="' + mapping[i] + '"]');
|
|
332
|
+
if (picker) picker.value = hex;
|
|
333
|
+
if (text) text.value = hex;
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
showToast('Palette applied to live preview');
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/* ── Reset Live Preview ────────────────────────────────────── */
|
|
341
|
+
var resetLiveBtn = document.getElementById('live-reset');
|
|
342
|
+
if (resetLiveBtn) {
|
|
343
|
+
resetLiveBtn.addEventListener('click', function () {
|
|
344
|
+
Object.keys(state.liveOverrides).forEach(function (name) {
|
|
345
|
+
document.documentElement.style.removeProperty(name);
|
|
346
|
+
});
|
|
347
|
+
state.liveOverrides = {};
|
|
348
|
+
renderLiveEditor();
|
|
349
|
+
showToast('Reset to defaults');
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/* ── Full YAML Export (combines skin + colors + overrides) ── */
|
|
354
|
+
window.rebuildFullYaml = function () {
|
|
355
|
+
var lines = [];
|
|
356
|
+
var skinEl = document.querySelector('.skin-card.border-primary');
|
|
357
|
+
var skin = skinEl ? skinEl.dataset.skin : 'dark';
|
|
358
|
+
lines.push('theme_skin: "' + skin + '"');
|
|
359
|
+
lines.push('');
|
|
360
|
+
lines.push('theme_color:');
|
|
361
|
+
|
|
362
|
+
// Colors from override state or the color editor
|
|
363
|
+
var colorVars = ['--bs-primary', '--bs-secondary', '--bs-success', '--bs-info', '--bs-warning', '--bs-danger'];
|
|
364
|
+
var colorNames = ['primary', 'secondary', 'success', 'info', 'warning', 'danger'];
|
|
365
|
+
colorVars.forEach(function (v, i) {
|
|
366
|
+
var val = state.liveOverrides[v] || readCurrentCSSVar(v);
|
|
367
|
+
try { val = chroma(val).hex(); } catch (e) { /* keep raw */ }
|
|
368
|
+
lines.push(' ' + colorNames[i] + ': "' + val + '"');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
lines.push('');
|
|
372
|
+
lines.push('# Layout overrides');
|
|
373
|
+
['--bs-border-radius', '--bs-border-width', '--bs-body-font-size', '--bs-body-font-weight', '--bs-body-line-height'].forEach(function (v) {
|
|
374
|
+
if (state.liveOverrides[v]) {
|
|
375
|
+
var key = v.replace('--bs-', '').replace(/-/g, '_');
|
|
376
|
+
lines.push('# ' + key + ': ' + state.liveOverrides[v]);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
var output = document.getElementById('theme-yaml-output');
|
|
381
|
+
if (output) output.textContent = lines.join('\n');
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
/* ── Toast Helper ──────────────────────────────────────────── */
|
|
385
|
+
function showToast(message) {
|
|
386
|
+
var existing = document.getElementById('palette-toast');
|
|
387
|
+
if (existing) existing.remove();
|
|
388
|
+
|
|
389
|
+
var toast = document.createElement('div');
|
|
390
|
+
toast.id = 'palette-toast';
|
|
391
|
+
toast.className = 'position-fixed bottom-0 end-0 m-3 p-3 bg-dark text-white rounded-3 shadow-lg';
|
|
392
|
+
toast.style.zIndex = '9999';
|
|
393
|
+
toast.style.transition = 'opacity .3s';
|
|
394
|
+
toast.textContent = message;
|
|
395
|
+
document.body.appendChild(toast);
|
|
396
|
+
setTimeout(function () {
|
|
397
|
+
toast.style.opacity = '0';
|
|
398
|
+
setTimeout(function () { toast.remove(); }, 300);
|
|
399
|
+
}, 2000);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/* ── Listen for theme mode changes → re-read computed styles ─ */
|
|
403
|
+
new MutationObserver(function (mutations) {
|
|
404
|
+
mutations.forEach(function (m) {
|
|
405
|
+
if (m.attributeName === 'data-bs-theme') {
|
|
406
|
+
// After theme switch, refresh the live editor defaults
|
|
407
|
+
setTimeout(renderLiveEditor, 100);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
}).observe(document.documentElement, { attributes: true, attributeFilter: ['data-bs-theme'] });
|
|
411
|
+
|
|
412
|
+
/* ── Init ──────────────────────────────────────────────────── */
|
|
413
|
+
renderPalette();
|
|
414
|
+
renderLiveEditor();
|
|
415
|
+
});
|
data/assets/js/search-modal.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Search Modal Controller
|
|
3
3
|
* - Opens modal on navigation:searchRequest event ("/" shortcut)
|
|
4
4
|
* - Focuses search input on open
|
|
5
|
-
* - Mutually exclusive with Settings (#info-section) and cookie settings modal so Bootstrap
|
|
6
|
-
* does not stack multiple
|
|
5
|
+
* - Mutually exclusive with Settings (#info-section offcanvas) and cookie settings modal so Bootstrap
|
|
6
|
+
* does not stack multiple backdrop layers (search vs Settings conflict).
|
|
7
7
|
*/
|
|
8
8
|
(function() {
|
|
9
9
|
'use strict';
|
|
@@ -29,6 +29,27 @@
|
|
|
29
29
|
inst.hide();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* If offcanvas is visible, hide it and run next() on hidden.bs.offcanvas; else run next() now.
|
|
34
|
+
*/
|
|
35
|
+
function afterOffcanvasClosed(offcanvasEl, next) {
|
|
36
|
+
if (!offcanvasEl || typeof bootstrap === 'undefined') {
|
|
37
|
+
next();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!offcanvasEl.classList.contains('show')) {
|
|
41
|
+
next();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const inst = bootstrap.Offcanvas.getInstance(offcanvasEl);
|
|
45
|
+
if (!inst) {
|
|
46
|
+
next();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
offcanvasEl.addEventListener('hidden.bs.offcanvas', next, { once: true });
|
|
50
|
+
inst.hide();
|
|
51
|
+
}
|
|
52
|
+
|
|
32
53
|
function initSearchModal() {
|
|
33
54
|
const modalEl = document.getElementById('siteSearchModal');
|
|
34
55
|
if (!modalEl) return;
|
|
@@ -52,20 +73,20 @@
|
|
|
52
73
|
const cookieEl = document.getElementById('cookieSettingsModal');
|
|
53
74
|
const infoEl = document.getElementById('info-section');
|
|
54
75
|
afterModalClosed(cookieEl, () => {
|
|
55
|
-
|
|
76
|
+
afterOffcanvasClosed(infoEl, showSearchModal);
|
|
56
77
|
});
|
|
57
78
|
};
|
|
58
79
|
|
|
59
80
|
const infoSectionEl = document.getElementById('info-section');
|
|
60
81
|
if (infoSectionEl) {
|
|
61
82
|
infoSectionEl.addEventListener(
|
|
62
|
-
'show.bs.
|
|
83
|
+
'show.bs.offcanvas',
|
|
63
84
|
(e) => {
|
|
64
85
|
if (!modalEl.classList.contains('show')) return;
|
|
65
86
|
e.preventDefault();
|
|
66
87
|
e.stopImmediatePropagation();
|
|
67
88
|
afterModalClosed(modalEl, () => {
|
|
68
|
-
bootstrap.
|
|
89
|
+
bootstrap.Offcanvas.getOrCreateInstance(infoSectionEl).show();
|
|
69
90
|
});
|
|
70
91
|
},
|
|
71
92
|
true,
|
|
@@ -193,12 +214,11 @@
|
|
|
193
214
|
}
|
|
194
215
|
|
|
195
216
|
function escapeHtml(value) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
.replace(/'/g, ''');
|
|
217
|
+
// Use the browser's built-in text escaping via DOM API
|
|
218
|
+
// instead of manual regex replacement chains (more secure, handles all edge cases)
|
|
219
|
+
const div = document.createElement('div');
|
|
220
|
+
div.textContent = String(value);
|
|
221
|
+
return div.innerHTML;
|
|
202
222
|
}
|
|
203
223
|
|
|
204
224
|
function highlightText(text, query) {
|