Spaces:
Running
Running
| // Theme & color system with "undefined" tokens and safe fallbacks | |
| (() => { | |
| const DEFAULT_PRIMARY = [59, 130, 246]; // blue-500 | |
| const DEFAULT_SECONDARY = [236, 72, 153]; // pink-500 | |
| const docEl = document.documentElement; | |
| function toRgbArray(input) { | |
| // Accepts: "59 130 246", "59,130,246", "rgb(59 130 246)", "#3b82f6" | |
| if (!input) return null; | |
| const trimmed = String(input).trim(); | |
| // Hex (#RRGGBB or #RGB) | |
| if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(trimmed)) { | |
| let hex = trimmed.replace('#', ''); | |
| if (hex.length === 3) { | |
| hex = hex.split('').map(c => c + c).join(''); | |
| } | |
| const int = parseInt(hex, 16); | |
| return [(int >> 16) & 255, (int >> 8) & 255, int & 255]; | |
| } | |
| // rgb(...) or space-separated | |
| const rgbMatch = | |
| trimmed.match(/^rgb\(\s*([0-9]{1,3})\s*[, ]\s*([0-9]{1,3})\s*[, ]\s*([0-9]{1,3})\s*\)$/i) || | |
| trimmed.match(/^([0-9]{1,3})\s+([0-9]{1,3})\s+([0-9]{1,3})$/); | |
| if (rgbMatch) { | |
| const r = Math.max(0, Math.min(255, parseInt(rgbMatch[1], 10))); | |
| const g = Math.max(0, Math.min(255, parseInt(rgbMatch[2], 10))); | |
| const b = Math.max(0, Math.min(255, parseInt(rgbMatch[3], 10))); | |
| return [r, g, b]; | |
| } | |
| // Comma-separated "R,G,B" | |
| const csv = trimmed.split(',').map(n => parseInt(n, 10)); | |
| if (csv.length === 3 && csv.every(n => Number.isFinite(n))) { | |
| return csv.map(n => Math.max(0, Math.min(255, n))); | |
| } | |
| return null; | |
| } | |
| function setColorVar(name, rgbArr) { | |
| const val = rgbArr ? `${rgbArr[0]} ${rgbArr[1]} ${rgbArr[2]}` : ''; | |
| docEl.style.setProperty(name, val); | |
| } | |
| function applyPrimaryColor(input) { | |
| const arr = toRgbArray(input) || DEFAULT_PRIMARY; | |
| setColorVar('--color-primary', arr); | |
| try { | |
| localStorage.setItem('primary', arr.join(' ')); | |
| } catch {} | |
| return arr; | |
| } | |
| function applySecondaryColor(input) { | |
| const arr = toRgbArray(input) || DEFAULT_SECONDARY; | |
| setColorVar('--color-secondary', arr); | |
| try { | |
| localStorage.setItem('secondary', arr.join(' ')); | |
| } catch {} | |
| return arr; | |
| } | |
| function applyThemeMode(mode) { | |
| // Accepts "light" or "dark" | |
| const m = mode === 'dark' ? 'dark' : 'light'; | |
| docEl.setAttribute('data-theme', m); | |
| try { | |
| localStorage.setItem('theme', m); | |
| } catch {} | |
| return m; | |
| } | |
| function currentTheme() { | |
| const m = docEl.getAttribute('data-theme') || 'light'; | |
| return m; | |
| } | |
| function toggleTheme() { | |
| const next = currentTheme() === 'dark' ? 'light' : 'dark'; | |
| applyThemeMode(next); | |
| } | |
| function boot() { | |
| // Apply theme preference from URL or localStorage or default to light ("undefined mode") | |
| const params = new URLSearchParams(location.search); | |
| const themeFromUrl = params.get('mode') || params.get('theme'); | |
| const theme = themeFromUrl === 'dark' ? 'dark' : 'light'; | |
| applyThemeMode(theme); | |
| // Apply colors from URL -> localStorage -> defaults | |
| const primaryFromUrl = params.get('primary') || params.get('primaryColor') || params.get('p'); | |
| const secondaryFromUrl = params.get('secondary') || params.get('secondaryColor') || params.get('s'); | |
| let primary, secondary; | |
| if (primaryFromUrl) { | |
| primary = applyPrimaryColor(primaryFromUrl); | |
| } else { | |
| try { | |
| const stored = localStorage.getItem('primary'); | |
| if (stored) primary = applyPrimaryColor(stored); | |
| } catch {} | |
| if (!primary) primary = applyPrimaryColor(null); | |
| } | |
| if (secondaryFromUrl) { | |
| secondary = applySecondaryColor(secondaryFromUrl); | |
| } else { | |
| try { | |
| const stored = localStorage.getItem('secondary'); | |
| if (stored) secondary = applySecondaryColor(stored); | |
| } catch {} | |
| if (!secondary) secondary = applySecondaryColor(null); | |
| } | |
| // UI hooks | |
| const themeToggle = document.getElementById('themeToggle'); | |
| if (themeToggle) { | |
| themeToggle.addEventListener('click', () => { | |
| toggleTheme(); | |
| }); | |
| } | |
| const applyBtn = document.getElementById('applyColorsBtn'); | |
| const primaryInput = document.getElementById('primaryColor'); | |
| const secondaryInput = document.getElementById('secondaryColor'); | |
| if (applyBtn && primaryInput && secondaryInput) { | |
| // Initialize inputs with current values | |
| try { | |
| const sp = getComputedStyle(docEl).getPropertyValue('--color-primary').trim() || DEFAULT_PRIMARY.join(' '); | |
| const ss = getComputedStyle(docEl).getPropertyValue('--color-secondary').trim() || DEFAULT_SECONDARY.join(' '); | |
| primaryInput.value = sp; | |
| secondaryInput.value = ss; | |
| } catch {} | |
| applyBtn.addEventListener('click', () => { | |
| applyPrimaryColor(primaryInput.value); | |
| applySecondaryColor(secondaryInput.value); | |
| }); | |
| } | |
| // Expose a tiny API for integration | |
| window.ExcailTheme = { | |
| setPrimary: applyPrimaryColor, | |
| setSecondary: applySecondaryColor, | |
| setMode: applyThemeMode, | |
| toggleTheme, | |
| getMode: currentTheme, | |
| }; | |
| } | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', boot, { once: true }); | |
| } else { | |
| boot(); | |
| } | |
| })(); |