- Fixed chat footer background to use var(--surface) CSS variable instead of hardcoded colors, ensuring proper theme adaptation - Fixed theme dropdown background by changing from var(--input-bg) to var(--surface) in theme-sentient.css - Added CSS override rules in desktop.css to ensure dropdown uses correct background - Added JavaScript fix in theme-manager.js to automatically apply correct dropdown background on theme load This resolves issues where theme dropdown and chat footer had black backgrounds in light themes.
419 lines
14 KiB
JavaScript
419 lines
14 KiB
JavaScript
// Unified Theme Manager - Dropdown only, no light/dark toggle
|
|
const ThemeManager = (() => {
|
|
let currentThemeId = "default";
|
|
let subscribers = [];
|
|
|
|
// Bot ID to theme mapping (configured via config.csv theme-base field)
|
|
const botThemeMap = {
|
|
// Default bot uses light theme with brown accents
|
|
default: "light",
|
|
// Cristo bot uses typewriter theme (classic typewriter style)
|
|
cristo: "typewriter",
|
|
// Salesianos bot uses light theme with blue accents
|
|
salesianos: "light",
|
|
};
|
|
|
|
// Detect current bot from URL path
|
|
function getCurrentBotId() {
|
|
const path = window.location.pathname;
|
|
// Match patterns like /bot/cristo, /cristo, etc.
|
|
const match = path.match(/(?:\/bot\/)?([a-z0-9-]+)/i);
|
|
if (match && match[1]) {
|
|
return match[1].toLowerCase();
|
|
}
|
|
return "default";
|
|
}
|
|
|
|
const themes = [
|
|
{ id: "default", name: "🎨 Default", file: "light.css" },
|
|
{ id: "light", name: "☀️ Light", file: "light.css" },
|
|
{ id: "orange", name: "🍊 Orange", file: "orange.css" },
|
|
{ id: "cyberpunk", name: "🌃 Cyberpunk", file: "cyberpunk.css" },
|
|
{ id: "retrowave", name: "🌴 Retrowave", file: "retrowave.css" },
|
|
{ id: "vapordream", name: "💭 Vapor Dream", file: "vapordream.css" },
|
|
{ id: "y2kglow", name: "✨ Y2K", file: "y2kglow.css" },
|
|
{ id: "3dbevel", name: "🔲 3D Bevel", file: "3dbevel.css" },
|
|
{ id: "arcadeflash", name: "🕹️ Arcade", file: "arcadeflash.css" },
|
|
{ id: "discofever", name: "🪩 Disco", file: "discofever.css" },
|
|
{ id: "grungeera", name: "🎸 Grunge", file: "grungeera.css" },
|
|
{ id: "jazzage", name: "🎺 Jazz", file: "jazzage.css" },
|
|
{ id: "mellowgold", name: "🌻 Mellow", file: "mellowgold.css" },
|
|
{ id: "midcenturymod", name: "🏠 Mid Century", file: "midcenturymod.css" },
|
|
{
|
|
id: "polaroidmemories",
|
|
name: "📷 Polaroid",
|
|
file: "polaroidmemories.css",
|
|
},
|
|
{
|
|
id: "saturdaycartoons",
|
|
name: "📺 Cartoons",
|
|
file: "saturdaycartoons.css",
|
|
},
|
|
{ id: "seasidepostcard", name: "🏖️ Seaside", file: "seasidepostcard.css" },
|
|
{ id: "typewriter", name: "⌨️ Typewriter", file: "typewriter.css" },
|
|
{ id: "xeroxui", name: "📠 Xerox", file: "xeroxui.css" },
|
|
{ id: "xtreegold", name: "📁 XTree", file: "xtreegold.css" },
|
|
];
|
|
|
|
function loadTheme(id) {
|
|
const theme = themes.find((t) => t.id === id);
|
|
if (!theme) {
|
|
console.warn("Theme not found:", id);
|
|
return;
|
|
}
|
|
|
|
const old = document.getElementById("theme-css");
|
|
if (old) old.remove();
|
|
|
|
if (!theme.file) {
|
|
currentThemeId = "default";
|
|
const botId = getCurrentBotId();
|
|
localStorage.setItem(`gb-theme-${botId}`, "default");
|
|
// Re-enable sentient theme for default
|
|
document.documentElement.setAttribute("data-theme", "sentient");
|
|
updateDropdown();
|
|
return;
|
|
}
|
|
|
|
const link = document.createElement("link");
|
|
link.id = "theme-css";
|
|
link.rel = "stylesheet";
|
|
link.href = `/suite/public/themes/${theme.file}`;
|
|
link.onload = () => {
|
|
console.log("✓ Theme loaded:", theme.name);
|
|
currentThemeId = id;
|
|
const botId = getCurrentBotId();
|
|
localStorage.setItem(`gb-theme-${botId}`, id);
|
|
|
|
// Keep data-theme="sentient" on html so CSS selectors work
|
|
// The inline styles will override the colors
|
|
if (!document.documentElement.getAttribute("data-theme")) {
|
|
document.documentElement.setAttribute("data-theme", "sentient");
|
|
}
|
|
|
|
// Remove data-theme from body to prevent base.css theme rules from overriding
|
|
document.body.removeAttribute("data-theme");
|
|
|
|
// Small delay to ensure CSS variables are applied
|
|
setTimeout(() => {
|
|
// Get the theme's colors from CSS variables
|
|
const rootStyle = getComputedStyle(document.documentElement);
|
|
const primary =
|
|
rootStyle.getPropertyValue("--primary")?.trim() || "#3b82f6";
|
|
const background =
|
|
rootStyle.getPropertyValue("--background")?.trim() || "0 0% 100%";
|
|
const foreground =
|
|
rootStyle.getPropertyValue("--foreground")?.trim() || "222 47% 11%";
|
|
const card = rootStyle.getPropertyValue("--card")?.trim() || "0 0% 98%";
|
|
const border =
|
|
rootStyle.getPropertyValue("--border")?.trim() || "214 32% 91%";
|
|
|
|
// Convert HSL values to hex format for app compatibility
|
|
const hslToHex = (h, s, l) => {
|
|
l /= 100;
|
|
const a = (s * Math.min(l, 1 - l)) / 100;
|
|
const f = (n) => {
|
|
const k = (n + h / 30) % 12;
|
|
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
|
return Math.round(255 * color)
|
|
.toString(16)
|
|
.padStart(2, "0");
|
|
};
|
|
return `#${f(0)}${f(8)}${f(4)}`;
|
|
};
|
|
|
|
const parseHsl = (hslStr) => {
|
|
if (!hslStr) return null;
|
|
const match = hslStr
|
|
.trim()
|
|
.match(/([0-9.]+)\s+([0-9.]+)%\s+([0-9.]+)%/);
|
|
if (match) {
|
|
return [
|
|
parseFloat(match[1]),
|
|
parseFloat(match[2]),
|
|
parseFloat(match[3]),
|
|
];
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const getContrastYIQ = (hexcolor) => {
|
|
if (!hexcolor) return "#ffffff";
|
|
hexcolor = hexcolor.replace("#", "");
|
|
if (hexcolor.length === 3) {
|
|
hexcolor = hexcolor
|
|
.split("")
|
|
.map((c) => c + c)
|
|
.join("");
|
|
}
|
|
if (hexcolor.length !== 6) return "#ffffff";
|
|
var r = parseInt(hexcolor.substr(0, 2), 16);
|
|
var g = parseInt(hexcolor.substr(2, 2), 16);
|
|
var b = parseInt(hexcolor.substr(4, 2), 16);
|
|
var yiq = (r * 299 + g * 587 + b * 114) / 1000;
|
|
return yiq >= 128 ? "#000000" : "#ffffff";
|
|
};
|
|
|
|
const bgHsl = parseHsl(background);
|
|
const fgHsl = parseHsl(foreground);
|
|
const cardHsl = parseHsl(card);
|
|
const borderHsl = parseHsl(border);
|
|
|
|
let calculatedTextHex = "#ffffff";
|
|
|
|
if (bgHsl) {
|
|
const bgHex = hslToHex(...bgHsl);
|
|
document.documentElement.style.setProperty("--bg", bgHex);
|
|
document.documentElement.style.setProperty("--bg-secondary", bgHex);
|
|
document.documentElement.style.setProperty(
|
|
"--primary-bg",
|
|
`hsl(${background})`,
|
|
);
|
|
document.documentElement.style.setProperty("--header-bg", bgHex);
|
|
document.documentElement.style.setProperty("--glass-bg", bgHex);
|
|
document.documentElement.style.setProperty("--sidebar-bg", bgHex);
|
|
calculatedTextHex = getContrastYIQ(bgHex);
|
|
}
|
|
if (fgHsl) {
|
|
const textHex = hslToHex(...fgHsl);
|
|
document.documentElement.style.setProperty("--text", textHex);
|
|
document.documentElement.style.setProperty("--text-primary", textHex);
|
|
document.documentElement.style.setProperty(
|
|
"--text-secondary",
|
|
textHex,
|
|
);
|
|
document.documentElement.style.setProperty("--text-muted", textHex);
|
|
document.documentElement.style.setProperty(
|
|
"--primary-fg",
|
|
`hsl(${foreground})`,
|
|
);
|
|
} else if (bgHsl) {
|
|
document.documentElement.style.setProperty(
|
|
"--text",
|
|
calculatedTextHex,
|
|
);
|
|
document.documentElement.style.setProperty(
|
|
"--text-primary",
|
|
calculatedTextHex,
|
|
);
|
|
document.documentElement.style.setProperty(
|
|
"--text-secondary",
|
|
calculatedTextHex,
|
|
);
|
|
document.documentElement.style.setProperty(
|
|
"--text-muted",
|
|
calculatedTextHex,
|
|
);
|
|
}
|
|
if (cardHsl) {
|
|
const surfaceHex = hslToHex(...cardHsl);
|
|
document.documentElement.style.setProperty("--surface", surfaceHex);
|
|
document.documentElement.style.setProperty(
|
|
"--surface-hover",
|
|
surfaceHex,
|
|
);
|
|
document.documentElement.style.setProperty(
|
|
"--surface-active",
|
|
surfaceHex,
|
|
);
|
|
document.documentElement.style.setProperty("--card-bg", surfaceHex);
|
|
}
|
|
if (borderHsl) {
|
|
const borderHex = hslToHex(...borderHsl);
|
|
document.documentElement.style.setProperty("--border", borderHex);
|
|
document.documentElement.style.setProperty(
|
|
"--border-light",
|
|
borderHex,
|
|
);
|
|
}
|
|
// Check if config.csv already set the primary color, we shouldn't wipe it
|
|
// Only update color and suggestion variables if they aren't marked as bot-config
|
|
if (
|
|
document.documentElement.getAttribute("data-has-bot-colors") !==
|
|
"true"
|
|
) {
|
|
document.documentElement.style.setProperty(
|
|
"--chat-color1",
|
|
`hsl(${primary})`,
|
|
);
|
|
document.documentElement.style.setProperty(
|
|
"--chat-color2",
|
|
`hsl(${card})`,
|
|
);
|
|
document.documentElement.style.setProperty(
|
|
"--suggestion-color",
|
|
`hsl(${primary})`,
|
|
);
|
|
document.documentElement.style.setProperty(
|
|
"--suggestion-bg",
|
|
`hsl(${card})`,
|
|
);
|
|
document.documentElement.style.setProperty(
|
|
"--color1",
|
|
`hsl(${primary})`,
|
|
);
|
|
document.documentElement.style.setProperty(
|
|
"--color2",
|
|
`hsl(${card})`,
|
|
);
|
|
}
|
|
|
|
console.log("✓ Theme colors applied:", {
|
|
bg: background,
|
|
primary: primary,
|
|
});
|
|
updateDropdown();
|
|
|
|
// Fix theme dropdown background to use surface color
|
|
const themeDropdown = document.getElementById("themeDropdown");
|
|
if (themeDropdown) {
|
|
const surfaceColor = getComputedStyle(document.documentElement)
|
|
.getPropertyValue("--surface")
|
|
.trim();
|
|
if (surfaceColor) {
|
|
themeDropdown.style.setProperty(
|
|
"background",
|
|
surfaceColor,
|
|
"important",
|
|
);
|
|
}
|
|
}
|
|
|
|
subscribers.forEach((cb) => cb({ themeId: id, themeName: theme.name }));
|
|
}, 50);
|
|
};
|
|
link.onerror = () => console.error("✗ Failed:", theme.name);
|
|
document.head.appendChild(link);
|
|
}
|
|
|
|
function updateDropdown() {
|
|
const select = document.getElementById("themeDropdown");
|
|
if (select) select.value = currentThemeId;
|
|
}
|
|
|
|
function createDropdown() {
|
|
const select = document.createElement("select");
|
|
select.id = "themeDropdown";
|
|
select.className = "theme-dropdown";
|
|
themes.forEach((t) => {
|
|
const opt = document.createElement("option");
|
|
opt.value = t.id;
|
|
opt.textContent = t.name;
|
|
select.appendChild(opt);
|
|
});
|
|
select.value = currentThemeId;
|
|
select.onchange = (e) => loadTheme(e.target.value);
|
|
return select;
|
|
}
|
|
|
|
function init() {
|
|
// Ensure data-theme is set on html element so CSS selectors work
|
|
if (!document.documentElement.getAttribute("data-theme")) {
|
|
document.documentElement.setAttribute("data-theme", "sentient");
|
|
}
|
|
|
|
// First, load saved bot theme from config.csv (if available)
|
|
loadSavedTheme();
|
|
|
|
// Then load the UI theme (CSS theme)
|
|
// Priority: 1) localStorage user preference, 2) bot-specific theme, 3) default
|
|
const botId = getCurrentBotId();
|
|
let saved = localStorage.getItem(`gb-theme-${botId}`);
|
|
if (!saved || !themes.find((t) => t.id === saved)) {
|
|
// No user preference, try bot-specific theme
|
|
saved = botThemeMap[botId] || "light";
|
|
// Save to localStorage so it persists
|
|
localStorage.setItem(`gb-theme-${botId}`, saved);
|
|
}
|
|
if (!themes.find((t) => t.id === saved)) saved = "default";
|
|
currentThemeId = saved;
|
|
loadTheme(saved);
|
|
|
|
// Dropdown injection restored for the window manager
|
|
const container = document.getElementById("themeSelectorContainer");
|
|
if (container) {
|
|
container.innerHTML = "";
|
|
container.appendChild(createDropdown());
|
|
}
|
|
|
|
console.log("✓ Theme Manager initialized");
|
|
}
|
|
|
|
function setThemeFromServer(data) {
|
|
// Save theme to localStorage for persistence across page loads
|
|
const botId = getCurrentBotId();
|
|
localStorage.setItem(`gb-theme-data-${botId}`, JSON.stringify(data));
|
|
|
|
// Load base theme if specified
|
|
if (data.theme_base) {
|
|
loadTheme(data.theme_base);
|
|
}
|
|
|
|
if (data.logo_url) {
|
|
// For img elements - set src and show, hide SVG
|
|
const logoImg = document.querySelector(".logo-icon-img");
|
|
const logoSvg = document.querySelector(".logo-icon-svg");
|
|
if (logoImg && logoSvg) {
|
|
logoImg.src = data.logo_url;
|
|
logoImg.alt = data.title || "Logo";
|
|
logoImg.style.display = "block";
|
|
logoSvg.style.display = "none";
|
|
}
|
|
|
|
// For elements that use background image
|
|
document.querySelectorAll(".assistant-avatar").forEach((el) => {
|
|
el.style.backgroundImage = `url("${data.logo_url}")`;
|
|
el.style.backgroundSize = "contain";
|
|
el.style.backgroundRepeat = "no-repeat";
|
|
el.style.backgroundPosition = "center";
|
|
});
|
|
}
|
|
if (data.color1) {
|
|
document.documentElement.style.setProperty("--color1", data.color1);
|
|
}
|
|
if (data.color2) {
|
|
document.documentElement.style.setProperty("--color2", data.color2);
|
|
}
|
|
if (data.title) document.title = data.title;
|
|
if (data.logo_text) {
|
|
document.querySelectorAll(".logo span, .logo-text").forEach((el) => {
|
|
el.textContent = data.logo_text;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Load saved theme from localStorage on page load
|
|
function loadSavedTheme() {
|
|
const botId = getCurrentBotId();
|
|
const savedTheme = localStorage.getItem(`gb-theme-data-${botId}`);
|
|
if (savedTheme) {
|
|
try {
|
|
const data = JSON.parse(savedTheme);
|
|
setThemeFromServer(data);
|
|
console.log(`✓ Theme loaded from localStorage for ${botId}`);
|
|
} catch (e) {
|
|
console.warn("Failed to load saved theme:", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
function applyCustomizations() {
|
|
// Called by modules if needed
|
|
}
|
|
|
|
function subscribe(cb) {
|
|
subscribers.push(cb);
|
|
}
|
|
|
|
return {
|
|
init,
|
|
loadTheme,
|
|
setThemeFromServer,
|
|
loadSavedTheme,
|
|
applyCustomizations,
|
|
subscribe,
|
|
getAvailableThemes: () => themes,
|
|
};
|
|
})();
|
|
|
|
window.ThemeManager = ThemeManager;
|