1/** 2 * CitrusOS theme chrome â palette tokens, tray sync, meta theme-color. 3 * Uses the active palette id (not computed CSS) so every settings change re-applies. 4 */ 5(function (global) { 6 'use strict'; 7 8 var probe = null; 9 10 function getProbe() { 11 if (!probe && document.body) { 12 probe = document.createElement('span'); 13 probe.setAttribute('aria-hidden', 'true'); 14 probe.style.cssText = 'position:absolute;left:-9999px;top:-9999px;opacity:0;pointer-events:none;'; 15 document.body.appendChild(probe); 16 } 17 return probe; 18 } 19 20 function parseRgbString(value) { 21 if (!value) return null; 22 var match = value.match(/rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i); 23 if (!match) return null; 24 return { 25 r: Math.round(parseFloat(match[1], 10)), 26 g: Math.round(parseFloat(match[2], 10)), 27 b: Math.round(parseFloat(match[3], 10)) 28 }; 29 } 30 31 function hexToRgb(hex) { 32 if (!hex) return null; 33 var clean = String(hex).replace('#', ''); 34 if (clean.length === 3) { 35 clean = clean[0] + clean[0] + clean[1] + clean[1] + clean[2] + clean[2]; 36 } 37 if (clean.length !== 6) return null; 38 return { 39 r: parseInt(clean.slice(0, 2), 16), 40 g: parseInt(clean.slice(2, 4), 16), 41 b: parseInt(clean.slice(4, 6), 16) 42 }; 43 } 44 45 function readPaletteId() { 46 return document.documentElement.getAttribute('data-site-palette') || 'default'; 47 } 48 49 function readAccentRgbFromPalette(paletteId) { 50 var api = global.SiteThemePalettes; 51 if (api && typeof api.getPaletteOption === 'function') { 52 var option = api.getPaletteOption(paletteId); 53 if (option && option.stripes && option.stripes[0]) { 54 var fromHex = hexToRgb(option.stripes[0]); 55 if (fromHex) return fromHex; 56 } 57 } 58 59 var node = getProbe(); 60 var root = document.documentElement; 61 if (!node || !root) return null; 62 63 var parent = node.parentNode; 64 root.appendChild(node); 65 node.style.color = 'var(--accent-primary)'; 66 var rgb = parseRgbString(getComputedStyle(node).color); 67 node.style.color = ''; 68 if (parent) parent.appendChild(node); 69 return rgb; 70 } 71 72 function rgbToHsl(r, g, b) { 73 r /= 255; 74 g /= 255; 75 b /= 255; 76 var max = Math.max(r, g, b); 77 var min = Math.min(r, g, b); 78 var h = 0; 79 var s = 0; 80 var l = (max + min) / 2; 81 82 if (max !== min) { 83 var d = max - min; 84 s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 85 switch (max) { 86 case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break; 87 case g: h = ((b - r) / d + 2) / 6; break; 88 default: h = ((r - g) / d + 4) / 6; break; 89 } 90 } 91 92 return { h: h * 360, s: s, l: l }; 93 } 94 95 function hslToRgb(h, s, l) { 96 h = ((h % 360) + 360) % 360; 97 var c = (1 - Math.abs(2 * l - 1)) * s; 98 var x = c * (1 - Math.abs((h / 60) % 2 - 1)); 99 var m = l - c / 2; 100 var r = 0; 101 var g = 0; 102 var b = 0; 103 104 if (h < 60) { r = c; g = x; } 105 else if (h < 120) { r = x; g = c; } 106 else if (h < 180) { g = c; b = x; } 107 else if (h < 240) { g = x; b = c; } 108 else if (h < 300) { r = x; b = c; } 109 else { r = c; b = x; } 110 111 return { 112 r: Math.round((r + m) * 255), 113 g: Math.round((g + m) * 255), 114 b: Math.round((b + m) * 255) 115 }; 116 } 117 118 function rgbToCss(rgb) { 119 return 'rgb(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ')'; 120 } 121 122 function isWarmAccent(hsl) { 123 return hsl.h < 58 || hsl.h >= 328; 124 } 125 126 function isCoolAccent(hsl) { 127 return hsl.h >= 135 && hsl.h <= 250; 128 } 129 130 function pickStartPalette(accentHsl, isDark) { 131 var warm = isWarmAccent(accentHsl); 132 var cool = isCoolAccent(accentHsl); 133 var saturated = accentHsl.s > 0.28; 134 135 if (warm && saturated) { 136 return { 137 hue: 198, 138 sat: isDark ? 0.52 : 0.48, 139 lightTop: isDark ? 0.46 : 0.42, 140 lightBottom: isDark ? 0.34 : 0.30, 141 text: '#ffffff' 142 }; 143 } 144 145 if (cool && saturated) { 146 return { 147 hue: 36, 148 sat: isDark ? 0.78 : 0.82, 149 lightTop: isDark ? 0.56 : 0.60, 150 lightBottom: isDark ? 0.44 : 0.48, 151 text: '#1a1208' 152 }; 153 } 154 155 return { 156 hue: 36, 157 sat: isDark ? 0.72 : 0.76, 158 lightTop: isDark ? 0.52 : 0.56, 159 lightBottom: isDark ? 0.40 : 0.44, 160 text: '#1a1208' 161 }; 162 } 163 164 function buildStartGradients(paletteId, isDark) { 165 var accentRgb = readAccentRgbFromPalette(paletteId); 166 if (!accentRgb) return null; 167 168 var accentHsl = rgbToHsl(accentRgb.r, accentRgb.g, accentRgb.b); 169 var palette = pickStartPalette(accentHsl, isDark); 170 var top = hslToRgb(palette.hue, palette.sat, palette.lightTop); 171 var bottom = hslToRgb(palette.hue, palette.sat * 0.95, palette.lightBottom); 172 var topActive = hslToRgb(palette.hue, palette.sat * 0.95, palette.lightBottom); 173 var bottomActive = hslToRgb(palette.hue, palette.sat, palette.lightTop); 174 175 return { 176 bg: 'linear-gradient(180deg, ' + rgbToCss(top) + ' 0%, ' + rgbToCss(bottom) + ' 100%)', 177 bgActive: 'linear-gradient(180deg, ' + rgbToCss(topActive) + ' 0%, ' + rgbToCss(bottomActive) + ' 100%)', 178 text: palette.text 179 }; 180 } 181 182 function syncTrayThemeIcon(isDark) { 183 var themeBtn = document.getElementById('citrusos-theme-btn'); 184 if (!themeBtn) return; 185 var icon = themeBtn.querySelector('i'); 186 if (icon) { 187 icon.className = isDark ? 'fa-solid fa-moon' : 'fa-solid fa-sun'; 188 } 189 } 190 191 function syncMetaThemeColor() { 192 var meta = document.querySelector('meta[name="theme-color"]'); 193 if (!meta || !document.body) return; 194 try { 195 var accent = getComputedStyle(document.body).getPropertyValue('--citrusos-accent-deep').trim(); 196 if (accent) meta.setAttribute('content', accent); 197 } catch (e) { /* ignore */ } 198 } 199 200 function applyStartButtonChrome(paletteId, isDark) { 201 if (!document.body) return; 202 var gradients = buildStartGradients(paletteId, isDark); 203 if (!gradients) return; 204 205 document.body.style.setProperty('--citrusos-start-bg', gradients.bg); 206 document.body.style.setProperty('--citrusos-start-bg-active', gradients.bgActive); 207 document.body.style.setProperty('--citrusos-start-text', gradients.text); 208 209 var startBtn = document.getElementById('citrusos-start-btn'); 210 if (startBtn) { 211 startBtn.style.removeProperty('background'); 212 startBtn.style.removeProperty('color'); 213 } 214 } 215 216 function apply(themeHint) { 217 if (!document.body || !document.body.classList.contains('page-citrusos')) return; 218 219 var paletteId = (themeHint && themeHint.palette) || readPaletteId();
220 var isDark = typeof (themeHint && themeHint.darkMode) === 'boolean' 221 ? themeHint.darkMode 222 : document.documentElement.classList.contains('dark-mode'); 223 224 syncTrayThemeIcon(isDark); 225 syncMetaThemeColor(); 226 applyStartButtonChrome(paletteId, isDark); 227 } 228 229 global.CitrusThemeChrome = { 230 apply: apply 231 }; 232 233 if (document.readyState === 'loading') { 234 document.addEventListener('DOMContentLoaded', function () { apply(); }); 235 } else { 236 apply(); 237 } 238})(typeof window !== 'undefined' ? window : this);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.