Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
376
status/js/theme.js
Normal file
376
status/js/theme.js
Normal file
@@ -0,0 +1,376 @@
|
||||
// ========== THEME CONTROLLER ==========
|
||||
// User-created themes are stored server-side as individual JSON files.
|
||||
// localStorage caches them for instant load; server is source of truth.
|
||||
(function () {
|
||||
var THEME_KEY = 'theme';
|
||||
var USER_THEMES_KEY = 'user-themes';
|
||||
var LEGACY_CUSTOM_KEY = 'custom-theme';
|
||||
|
||||
var BUILTIN_THEMES = ['dark', 'light', 'blue', 'black', 'nord', 'dracula', 'solarized-dark', 'solarized-light', 'taxi', 'ocean'];
|
||||
var THEMES = BUILTIN_THEMES.slice();
|
||||
|
||||
var THEME_PROPS = [
|
||||
'bg', 'fg', 'muted', 'fg-muted', 'card-base', 'card-bg', 'border',
|
||||
'hover', 'card-hover', 'base',
|
||||
'ok-bg', 'ok-fg', 'bad-bg', 'bad-fg', 'dot-ok', 'dot-bad', 'uptime',
|
||||
'success', 'error', 'warning',
|
||||
'accent', 'accent-strong'
|
||||
];
|
||||
|
||||
// Base props the user picks in the builder (subset of THEME_PROPS)
|
||||
var BASE_PROPS = [
|
||||
'bg', 'fg', 'muted', 'card-base', 'card-bg', 'border',
|
||||
'ok-bg', 'ok-fg', 'bad-bg', 'bad-fg', 'dot-ok', 'dot-bad', 'uptime',
|
||||
'accent', 'accent-strong'
|
||||
];
|
||||
|
||||
// Props that are auto-derived if not explicitly set
|
||||
var DERIVED_PROPS = ['fg-muted', 'hover', 'card-hover', 'base', 'success', 'error', 'warning'];
|
||||
|
||||
var THEME_COLORS = {
|
||||
dark: { bg: '#0b0f1a', fg: '#e8ecf5', muted: '#9aa6bf', 'fg-muted': '#6b7a94', 'card-base': '#121826', 'card-bg': '#121826', border: '#263552', hover: '#1a2235', 'card-hover': '#161e2e', base: '#151c2b', 'ok-bg': '#0c2430', 'ok-fg': '#7ef2ff', 'bad-bg': '#2a121a', 'bad-fg': '#ff9aa3', 'dot-ok': '#35d1ff', 'dot-bad': '#ff5f7a', uptime: '#35d1ff', success: '#4caf50', error: '#e74c3c', warning: '#f39c12', accent: '#8FD6FF', 'accent-strong': '#1F7BFF' },
|
||||
light: { bg: '#f6f7fb', fg: '#0f1115', muted: '#5f6b7a', 'fg-muted': '#8993a4', 'card-base': '#ffffff', 'card-bg': '#ffffff', border: '#e2e7ef', hover: '#eef1f6', 'card-hover': '#f5f6fa', base: '#ebeef3', 'ok-bg': '#eafff1', 'ok-fg': '#0a7c3a', 'bad-bg': '#ffefef', 'bad-fg': '#b00020', 'dot-ok': '#0fb15a', 'dot-bad': '#d93b3b', uptime: '#0fb15a', success: '#0a7c3a', error: '#b00020', warning: '#d68a00', accent: '#4a90d9', 'accent-strong': '#2563eb', lightBg: true },
|
||||
blue: { bg: '#1908AC', fg: '#e8f1ff', muted: '#d6e2ff', 'fg-muted': '#9eafdb', 'card-base': '#0d1533', 'card-bg': '#0d1533', border: '#1c2d6a', hover: '#141f4a', 'card-hover': '#111a3e', base: '#0f1840', 'ok-bg': '#162040', 'ok-fg': '#edffff', 'bad-bg': '#0a0e24', 'bad-fg': '#ffb3c0', 'dot-ok': '#c7e5ff', 'dot-bad': '#ffd6dc', uptime: '#7ec8ff', success: '#7ec8ff', error: '#ffb3c0', warning: '#ffd080', accent: '#9cd4ff', 'accent-strong': '#6fb2ff' },
|
||||
black: { bg: '#0e0e0e', fg: '#f5f5f5', muted: '#999999', 'fg-muted': '#666666', 'card-base': '#1a1a1a', 'card-bg': '#1a1a1a', border: '#2e2e2e', hover: '#242424', 'card-hover': '#202020', base: '#161616', 'ok-bg': '#0f2a12', 'ok-fg': '#66ff7a', 'bad-bg': '#2a0f0f', 'bad-fg': '#ff6b6b', 'dot-ok': '#4caf50', 'dot-bad': '#ff4444', uptime: '#e0e0e0', success: '#4caf50', error: '#ff4444', warning: '#ff9800', accent: '#E63946', 'accent-strong': '#C62828' },
|
||||
nord: { bg: '#2e3440', fg: '#eceff4', muted: '#81a1c1', 'fg-muted': '#6882a0', 'card-base': '#3b4252', 'card-bg': '#3b4252', border: '#4c566a', hover: '#434c5e', 'card-hover': '#3f4858', base: '#353c4a', 'ok-bg': '#2d4f3e', 'ok-fg': '#a3be8c', 'bad-bg': '#4a2c2a', 'bad-fg': '#bf616a', 'dot-ok': '#a3be8c', 'dot-bad': '#bf616a', uptime: '#a3be8c', success: '#a3be8c', error: '#bf616a', warning: '#ebcb8b', accent: '#88c0d0', 'accent-strong': '#5e81ac' },
|
||||
dracula: { bg: '#282a36', fg: '#f8f8f2', muted: '#6272a4', 'fg-muted': '#515d85', 'card-base': '#44475a', 'card-bg': '#44475a', border: '#6272a4', hover: '#4e5170', 'card-hover': '#494c63', base: '#363848', 'ok-bg': '#1e3a2e', 'ok-fg': '#50fa7b', 'bad-bg': '#3d1a1a', 'bad-fg': '#ff5555', 'dot-ok': '#50fa7b', 'dot-bad': '#ff5555', uptime: '#50fa7b', success: '#50fa7b', error: '#ff5555', warning: '#f1fa8c', accent: '#bd93f9', 'accent-strong': '#8be9fd' },
|
||||
'solarized-dark': { bg: '#002b36', fg: '#839496', muted: '#586e75', 'fg-muted': '#4a5f65', 'card-base': '#073642', 'card-bg': '#073642', border: '#586e75', hover: '#0d4050', 'card-hover': '#0a3a48', base: '#053340', 'ok-bg': '#0d3d2c', 'ok-fg': '#859900', 'bad-bg': '#3d1a1a', 'bad-fg': '#dc322f', 'dot-ok': '#859900', 'dot-bad': '#dc322f', uptime: '#b5bd68', success: '#859900', error: '#dc322f', warning: '#b58900', accent: '#268bd2', 'accent-strong': '#2aa198' },
|
||||
'solarized-light':{ bg: '#fdf6e3', fg: '#657b83', muted: '#93a1a1', 'fg-muted': '#adb8b8', 'card-base': '#eee8d5', 'card-bg': '#eee8d5', border: '#93a1a1', hover: '#e6dfcb', 'card-hover': '#eae3cf', base: '#e8e1cd', 'ok-bg': '#e8f5e8', 'ok-fg': '#859900', 'bad-bg': '#fdf2f2', 'bad-fg': '#dc322f', 'dot-ok': '#859900', 'dot-bad': '#dc322f', uptime: '#859900', success: '#859900', error: '#dc322f', warning: '#b58900', accent: '#268bd2', 'accent-strong': '#2aa198', lightBg: true },
|
||||
taxi: { bg: '#f3d321', fg: '#0e0e00', muted: '#4a4a10', 'fg-muted': '#6b6b30', 'card-base': '#ffd700', 'card-bg': '#ffd700', border: '#b8a840', hover: '#ffe84d', 'card-hover': '#ffe033', base: '#f0d000', 'ok-bg': '#d4ffd9', 'ok-fg': '#0f2a0f', 'bad-bg': '#ffd4d4', 'bad-fg': '#2a0f0f', 'dot-ok': '#4caf50', 'dot-bad': '#ff4444', uptime: '#0e0e00', success: '#2e7d32', error: '#c62828', warning: '#e65100', accent: '#0e0e00', 'accent-strong': '#1a1a05', lightBg: true },
|
||||
ocean: { bg: '#2060b0', fg: '#faf5eb', muted: '#dcd2c0', 'fg-muted': '#b0a890', 'card-base': '#7a94ed', 'card-bg': '#7a94ed', border: '#deb67a', hover: '#8aa0f0', 'card-hover': '#8298e8', base: '#6888e0', 'ok-bg': '#4f5bb0', 'ok-fg': '#c7d7eb', 'bad-bg': '#f41a3a', 'bad-fg': '#6a1818', 'dot-ok': '#30a050', 'dot-bad': '#d04040', uptime: '#2d32f2', success: '#30a050', error: '#d04040', warning: '#e6a030', accent: '#1860a0', 'accent-strong': '#104080' },
|
||||
};
|
||||
|
||||
// ─── Color helpers ───────────────────────────────
|
||||
|
||||
function hexToRgb(hex) {
|
||||
if (!hex || typeof hex !== 'string') return { r: 0, g: 0, b: 0 };
|
||||
hex = hex.replace('#', '');
|
||||
if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
|
||||
return {
|
||||
r: parseInt(hex.substr(0, 2), 16) || 0,
|
||||
g: parseInt(hex.substr(2, 2), 16) || 0,
|
||||
b: parseInt(hex.substr(4, 2), 16) || 0
|
||||
};
|
||||
}
|
||||
|
||||
function rgbToHex(r, g, b) {
|
||||
return '#' + [r, g, b].map(function (x) {
|
||||
var h = Math.max(0, Math.min(255, Math.round(x))).toString(16);
|
||||
return h.length === 1 ? '0' + h : h;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function blendColors(hex1, hex2, ratio) {
|
||||
var c1 = hexToRgb(hex1);
|
||||
var c2 = hexToRgb(hex2);
|
||||
return rgbToHex(
|
||||
c1.r + (c2.r - c1.r) * ratio,
|
||||
c1.g + (c2.g - c1.g) * ratio,
|
||||
c1.b + (c2.b - c1.b) * ratio
|
||||
);
|
||||
}
|
||||
|
||||
function hexLuminance(hex) {
|
||||
hex = hex.replace('#', '');
|
||||
if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
|
||||
var r = parseInt(hex.substr(0, 2), 16) / 255;
|
||||
var g = parseInt(hex.substr(2, 2), 16) / 255;
|
||||
var b = parseInt(hex.substr(4, 2), 16) / 255;
|
||||
r = r <= 0.03928 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4);
|
||||
g = g <= 0.03928 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4);
|
||||
b = b <= 0.03928 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4);
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
|
||||
// ─── Auto-derive extended colors from base palette ───────────
|
||||
|
||||
function deriveExtendedColors(colors) {
|
||||
var bg = colors.bg || '#0b0f1a';
|
||||
var fg = colors.fg || '#e8ecf5';
|
||||
var muted = colors.muted || '#9aa6bf';
|
||||
var cardBase = colors['card-base'] || colors.bg || '#121826';
|
||||
var dotOk = colors['dot-ok'] || '#4caf50';
|
||||
var dotBad = colors['dot-bad'] || '#e74c3c';
|
||||
var isLight = colors.lightBg || (bg && hexLuminance(bg) > 0.4);
|
||||
|
||||
var derived = {};
|
||||
|
||||
// hover: subtle shift of card-base toward fg (dark) or toward bg (light)
|
||||
derived.hover = isLight
|
||||
? blendColors(cardBase, bg, 0.35)
|
||||
: blendColors(cardBase, fg, 0.08);
|
||||
|
||||
// card-hover: midpoint between card-base and hover
|
||||
derived['card-hover'] = blendColors(cardBase, derived.hover, 0.5);
|
||||
|
||||
// base: subdued background between bg and card-base
|
||||
derived.base = blendColors(bg, cardBase, 0.6);
|
||||
|
||||
// fg-muted: dimmer than muted, blend toward bg
|
||||
derived['fg-muted'] = blendColors(muted, bg, 0.35);
|
||||
|
||||
// success: reuse dot-ok (the green of this theme)
|
||||
derived.success = dotOk;
|
||||
|
||||
// error: reuse dot-bad (the red of this theme)
|
||||
derived.error = dotBad;
|
||||
|
||||
// warning: warm amber, adjusted for contrast
|
||||
derived.warning = isLight ? '#d68a00' : '#f39c12';
|
||||
|
||||
return derived;
|
||||
}
|
||||
|
||||
// ─── Generate body background CSS for user themes ───────────
|
||||
|
||||
function generateBodyBackgroundCSS(slug, colors) {
|
||||
var isLight = colors.lightBg || (colors.bg && hexLuminance(colors.bg) > 0.4);
|
||||
var accent = colors.accent || colors['accent-strong'] || '#888888';
|
||||
var accentRgb = hexToRgb(accent);
|
||||
|
||||
if (isLight) {
|
||||
return ':root.' + slug + ' body {\n' +
|
||||
' background:\n' +
|
||||
' radial-gradient(1200px 800px at 10% -10%, rgba(' + accentRgb.r + ',' + accentRgb.g + ',' + accentRgb.b + ', .08), transparent 60%),\n' +
|
||||
' radial-gradient(1000px 700px at 110% 10%, rgba(' + accentRgb.r + ',' + accentRgb.g + ',' + accentRgb.b + ', .05), transparent 55%),\n' +
|
||||
' var(--bg);\n' +
|
||||
'}\n';
|
||||
} else {
|
||||
return ':root.' + slug + ' body {\n' +
|
||||
' background:\n' +
|
||||
' radial-gradient(1200px 900px at 8% -12%, rgba(' + accentRgb.r + ',' + accentRgb.g + ',' + accentRgb.b + ', .10), transparent 60%),\n' +
|
||||
' radial-gradient(1000px 700px at 110% -10%, rgba(' + accentRgb.r + ',' + accentRgb.g + ',' + accentRgb.b + ', .07), transparent 55%),\n' +
|
||||
' var(--bg);\n' +
|
||||
'}\n';
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Generate button hover CSS for user themes ───────────
|
||||
|
||||
function generateButtonHoverCSS(slug, colors) {
|
||||
var isLight = colors.lightBg || (colors.bg && hexLuminance(colors.bg) > 0.4);
|
||||
|
||||
if (isLight) {
|
||||
return ':root.' + slug + ' button:hover {\n' +
|
||||
' background: color-mix(in srgb, var(--accent-strong) 12%, white 88%);\n' +
|
||||
' border-color: rgba(0, 0, 0, .15);\n' +
|
||||
' box-shadow: 0 1px 6px rgba(0, 0, 0, .08), inset 0 1px 0 rgba(255, 255, 255, .8);\n' +
|
||||
'}\n';
|
||||
} else {
|
||||
return ':root.' + slug + ' button:hover {\n' +
|
||||
' background: color-mix(in srgb, var(--accent) 18%, transparent);\n' +
|
||||
' border-color: color-mix(in srgb, var(--accent) 35%, var(--border));\n' +
|
||||
'}\n';
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Core theme functions ───────────────────────────────
|
||||
|
||||
function getSystemTheme() {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
function clearCustomProperties() {
|
||||
THEME_PROPS.forEach(function (prop) {
|
||||
document.documentElement.style.removeProperty('--' + prop);
|
||||
});
|
||||
}
|
||||
|
||||
function slugify(name, currentSlug) {
|
||||
var slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||
if (!slug) slug = 'custom';
|
||||
if (BUILTIN_THEMES.indexOf(slug) !== -1) slug = slug + '-custom';
|
||||
var cached = safeGetJSON(USER_THEMES_KEY, {});
|
||||
var base = slug;
|
||||
var counter = 2;
|
||||
while (cached[slug] && slug !== currentSlug) {
|
||||
slug = base + '-' + counter++;
|
||||
}
|
||||
return slug;
|
||||
}
|
||||
|
||||
// Inject user themes from a themes object into DOM + runtime
|
||||
function injectUserThemeStyles(themesData) {
|
||||
var existing = document.getElementById('user-theme-styles');
|
||||
if (existing) existing.remove();
|
||||
|
||||
THEMES.length = BUILTIN_THEMES.length;
|
||||
Object.keys(THEME_COLORS).forEach(function (k) {
|
||||
if (BUILTIN_THEMES.indexOf(k) === -1) delete THEME_COLORS[k];
|
||||
});
|
||||
|
||||
// Use provided data, or fall back to localStorage cache
|
||||
var userThemes = themesData || safeGetJSON(USER_THEMES_KEY, {});
|
||||
var slugs = Object.keys(userThemes);
|
||||
|
||||
// Filter out any user themes that collide with built-in slugs
|
||||
slugs = slugs.filter(function (s) { return BUILTIN_THEMES.indexOf(s) === -1; });
|
||||
|
||||
if (!slugs.length) return;
|
||||
|
||||
var css = '';
|
||||
slugs.forEach(function (slug) {
|
||||
var theme = userThemes[slug];
|
||||
if (THEMES.indexOf(slug) === -1) THEMES.push(slug);
|
||||
|
||||
// Build color map from saved data
|
||||
var colorMap = {};
|
||||
THEME_PROPS.forEach(function (p) { if (theme[p]) colorMap[p] = theme[p]; });
|
||||
colorMap['card-bg'] = theme['card-base'] || theme.bg;
|
||||
if (theme.lightBg) colorMap.lightBg = true;
|
||||
|
||||
// Auto-derive any missing extended colors
|
||||
var derived = deriveExtendedColors(colorMap);
|
||||
DERIVED_PROPS.forEach(function (p) {
|
||||
if (!colorMap[p] && derived[p]) colorMap[p] = derived[p];
|
||||
});
|
||||
|
||||
THEME_COLORS[slug] = colorMap;
|
||||
|
||||
// Emit main variable block
|
||||
css += ':root.' + slug + ' {\n';
|
||||
THEME_PROPS.forEach(function (p) {
|
||||
if (colorMap[p]) css += ' --' + p + ': ' + colorMap[p] + ';\n';
|
||||
});
|
||||
css += '}\n';
|
||||
|
||||
// Emit body background gradient
|
||||
css += generateBodyBackgroundCSS(slug, colorMap);
|
||||
|
||||
// Emit button hover override
|
||||
css += generateButtonHoverCSS(slug, colorMap);
|
||||
});
|
||||
|
||||
var style = document.createElement('style');
|
||||
style.id = 'user-theme-styles';
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// Fetch themes from server, update cache, re-inject if changed
|
||||
function syncThemesFromServer() {
|
||||
secureFetch('/api/v1/themes').then(function (r) { return r.json(); }).then(function (data) {
|
||||
if (!data.success || !data.themes) return;
|
||||
var serverThemes = data.themes;
|
||||
var cached = safeGetJSON(USER_THEMES_KEY, {});
|
||||
// Only update if different
|
||||
if (JSON.stringify(serverThemes) !== JSON.stringify(cached)) {
|
||||
safeSet(USER_THEMES_KEY, JSON.stringify(serverThemes));
|
||||
injectUserThemeStyles(serverThemes);
|
||||
// Re-apply current theme in case it was just synced
|
||||
var current = safeGet(THEME_KEY);
|
||||
if (current && THEMES.indexOf(current) !== -1) {
|
||||
applyTheme(current);
|
||||
}
|
||||
}
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
// Migrate legacy custom-theme to server
|
||||
function migrateLegacyCustomTheme() {
|
||||
var legacy = safeGetJSON(LEGACY_CUSTOM_KEY);
|
||||
if (!legacy) return;
|
||||
|
||||
var name = legacy.name || 'Custom';
|
||||
var slug = slugify(name);
|
||||
|
||||
var themeData = { name: name };
|
||||
THEME_PROPS.forEach(function (p) {
|
||||
if (legacy[p]) themeData[p] = legacy[p];
|
||||
});
|
||||
|
||||
// Save to localStorage cache immediately
|
||||
var cached = safeGetJSON(USER_THEMES_KEY, {});
|
||||
cached[slug] = themeData;
|
||||
safeSet(USER_THEMES_KEY, JSON.stringify(cached));
|
||||
|
||||
// Update active theme reference
|
||||
if (safeGet(THEME_KEY) === 'custom') {
|
||||
safeSet(THEME_KEY, slug);
|
||||
}
|
||||
|
||||
safeRemove(LEGACY_CUSTOM_KEY);
|
||||
|
||||
// Also push to server
|
||||
var colors = {};
|
||||
THEME_PROPS.forEach(function (p) { if (themeData[p]) colors[p] = themeData[p]; });
|
||||
fetch('/api/v1/themes/' + slug, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: name, colors: colors })
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
function applyTheme(mode) {
|
||||
document.documentElement.classList.add('theme-transitioning');
|
||||
|
||||
THEMES.forEach(function (t) {
|
||||
if (t !== 'dark') document.documentElement.classList.remove(t);
|
||||
});
|
||||
|
||||
clearCustomProperties();
|
||||
|
||||
if (mode !== 'dark') {
|
||||
document.documentElement.classList.add(mode);
|
||||
}
|
||||
|
||||
safeSet(THEME_KEY, mode);
|
||||
|
||||
var colors = THEME_COLORS[mode];
|
||||
var meta = document.querySelector('meta[name="theme-color"]');
|
||||
if (meta && colors) meta.setAttribute('content', colors.bg);
|
||||
|
||||
// Toggle light-bg class: explicit flag first, then auto-detect from luminance
|
||||
var isLight = colors && colors.lightBg;
|
||||
if (!isLight && colors && colors.bg) isLight = hexLuminance(colors.bg) > 0.4;
|
||||
if (isLight) {
|
||||
document.documentElement.classList.add('light-bg');
|
||||
} else {
|
||||
document.documentElement.classList.remove('light-bg');
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
document.documentElement.classList.remove('theme-transitioning');
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// --- Initialization ---
|
||||
migrateLegacyCustomTheme();
|
||||
// Instant load from cache
|
||||
injectUserThemeStyles();
|
||||
|
||||
var savedTheme = safeGet(THEME_KEY);
|
||||
if (savedTheme === 'red') { savedTheme = 'black'; safeSet(THEME_KEY, 'black'); }
|
||||
if (savedTheme && savedTheme !== 'dark' && THEMES.indexOf(savedTheme) === -1) {
|
||||
savedTheme = null;
|
||||
}
|
||||
applyTheme(savedTheme || getSystemTheme());
|
||||
|
||||
// Sync from server in background (updates cache if server has newer data)
|
||||
syncThemesFromServer();
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function (e) {
|
||||
if (!safeGet(THEME_KEY)) {
|
||||
applyTheme(e.matches ? 'dark' : 'light');
|
||||
}
|
||||
});
|
||||
|
||||
// Expose API
|
||||
window.THEMES = THEMES;
|
||||
window.BUILTIN_THEMES = BUILTIN_THEMES;
|
||||
window.THEME_COLORS = THEME_COLORS;
|
||||
window.THEME_PROPS = THEME_PROPS;
|
||||
window.BASE_PROPS = BASE_PROPS;
|
||||
window.DERIVED_PROPS = DERIVED_PROPS;
|
||||
window.USER_THEMES_KEY = USER_THEMES_KEY;
|
||||
window.applyTheme = applyTheme;
|
||||
window.clearCustomProperties = clearCustomProperties;
|
||||
window.injectUserThemeStyles = injectUserThemeStyles;
|
||||
window.syncThemesFromServer = syncThemesFromServer;
|
||||
window.slugifyThemeName = slugify;
|
||||
window.getActiveTheme = function () { return safeGet(THEME_KEY) || getSystemTheme(); };
|
||||
window.deriveExtendedColors = deriveExtendedColors;
|
||||
window.hexToRgb = hexToRgb;
|
||||
window.rgbToHex = rgbToHex;
|
||||
window.blendColors = blendColors;
|
||||
})();
|
||||
Reference in New Issue
Block a user