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,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
+ });
@@ -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 .modal-backdrop layers (search vs About/Settings conflict).
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
- afterModalClosed(infoEl, showSearchModal);
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.modal',
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.Modal.getOrCreateInstance(infoSectionEl).show();
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
- return String(value)
197
- .replace(/&/g, '&amp;')
198
- .replace(/</g, '&lt;')
199
- .replace(/>/g, '&gt;')
200
- .replace(/"/g, '&quot;')
201
- .replace(/'/g, '&#39;');
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) {