2025-12-03 18:42:22 -03:00
|
|
|
// Unified Theme Manager - Dropdown only, no light/dark toggle
|
|
|
|
|
const ThemeManager = (() => {
|
|
|
|
|
let currentThemeId = "default";
|
|
|
|
|
let subscribers = [];
|
|
|
|
|
|
2026-02-10 13:54:16 +00:00
|
|
|
// Bot ID to theme mapping (configured via config.csv theme-base field)
|
|
|
|
|
const botThemeMap = {
|
|
|
|
|
// Default bot uses light theme with brown accents
|
|
|
|
|
"default": "light",
|
2026-02-15 23:48:40 +00:00
|
|
|
// Cristo bot uses typewriter theme (classic typewriter style)
|
|
|
|
|
"cristo": "typewriter",
|
2026-02-10 13:54:16 +00:00
|
|
|
// 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";
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 18:42:22 -03:00
|
|
|
const themes = [
|
2026-01-11 20:58:11 -03:00
|
|
|
{ id: "default", name: "🎨 Default", file: "light.css" },
|
|
|
|
|
{ id: "light", name: "☀️ Light", file: "light.css" },
|
2025-12-03 18:42:22 -03:00
|
|
|
{ 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" },
|
2026-01-11 20:58:11 -03:00
|
|
|
{
|
|
|
|
|
id: "polaroidmemories",
|
|
|
|
|
name: "📷 Polaroid",
|
|
|
|
|
file: "polaroidmemories.css",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "saturdaycartoons",
|
|
|
|
|
name: "📺 Cartoons",
|
|
|
|
|
file: "saturdaycartoons.css",
|
|
|
|
|
},
|
2025-12-03 18:42:22 -03:00
|
|
|
{ id: "seasidepostcard", name: "🏖️ Seaside", file: "seasidepostcard.css" },
|
|
|
|
|
{ id: "typewriter", name: "⌨️ Typewriter", file: "typewriter.css" },
|
|
|
|
|
{ id: "xeroxui", name: "📠 Xerox", file: "xeroxui.css" },
|
2026-01-11 20:58:11 -03:00
|
|
|
{ id: "xtreegold", name: "📁 XTree", file: "xtreegold.css" },
|
2025-12-03 18:42:22 -03:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function loadTheme(id) {
|
2026-01-11 20:58:11 -03:00
|
|
|
const theme = themes.find((t) => t.id === id);
|
2025-12-03 18:42:22 -03:00
|
|
|
if (!theme) {
|
|
|
|
|
console.warn("Theme not found:", id);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const old = document.getElementById("theme-css");
|
|
|
|
|
if (old) old.remove();
|
|
|
|
|
|
|
|
|
|
if (!theme.file) {
|
|
|
|
|
currentThemeId = "default";
|
|
|
|
|
localStorage.setItem("gb-theme", "default");
|
2026-02-15 23:19:16 +00:00
|
|
|
// Re-enable sentient theme for default
|
|
|
|
|
document.documentElement.setAttribute("data-theme", "sentient");
|
2025-12-03 18:42:22 -03:00
|
|
|
updateDropdown();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const link = document.createElement("link");
|
|
|
|
|
link.id = "theme-css";
|
|
|
|
|
link.rel = "stylesheet";
|
2026-02-15 21:33:18 +00:00
|
|
|
link.href = `/suite/public/themes/${theme.file}`;
|
2025-12-03 18:42:22 -03:00
|
|
|
link.onload = () => {
|
|
|
|
|
console.log("✓ Theme loaded:", theme.name);
|
|
|
|
|
currentThemeId = id;
|
|
|
|
|
localStorage.setItem("gb-theme", id);
|
2026-02-15 21:33:18 +00:00
|
|
|
|
2026-02-15 23:19:16 +00:00
|
|
|
// 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");
|
|
|
|
|
|
2026-02-15 21:53:50 +00:00
|
|
|
// Small delay to ensure CSS variables are applied
|
|
|
|
|
setTimeout(() => {
|
2026-02-15 23:19:16 +00:00
|
|
|
// Get the theme's colors from CSS variables
|
2026-02-15 21:53:50 +00:00
|
|
|
const rootStyle = getComputedStyle(document.documentElement);
|
2026-02-15 23:19:16 +00:00
|
|
|
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) => {
|
|
|
|
|
const match = hslStr.match(/(\d+)\s+(\d+)%\s+(\d+)%/);
|
|
|
|
|
if (match) {
|
|
|
|
|
return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])];
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const bgHsl = parseHsl(background);
|
|
|
|
|
const fgHsl = parseHsl(foreground);
|
|
|
|
|
const cardHsl = parseHsl(card);
|
|
|
|
|
const borderHsl = parseHsl(border);
|
|
|
|
|
|
|
|
|
|
// Update the app's CSS variables with the theme colors
|
|
|
|
|
// These inline styles override the theme-sentient.css values
|
|
|
|
|
if (bgHsl) {
|
|
|
|
|
const bgHex = hslToHex(...bgHsl);
|
|
|
|
|
document.documentElement.style.setProperty("--bg", bgHex);
|
|
|
|
|
document.documentElement.style.setProperty("--primary-bg", `hsl(${background})`);
|
|
|
|
|
document.documentElement.style.setProperty("--header-bg", bgHex);
|
|
|
|
|
}
|
|
|
|
|
if (fgHsl) {
|
|
|
|
|
const textHex = hslToHex(...fgHsl);
|
|
|
|
|
document.documentElement.style.setProperty("--text", textHex);
|
|
|
|
|
document.documentElement.style.setProperty("--primary-fg", `hsl(${foreground})`);
|
|
|
|
|
}
|
|
|
|
|
if (cardHsl) {
|
|
|
|
|
const surfaceHex = hslToHex(...cardHsl);
|
|
|
|
|
document.documentElement.style.setProperty("--surface", surfaceHex);
|
|
|
|
|
document.documentElement.style.setProperty("--card-bg", surfaceHex);
|
|
|
|
|
}
|
|
|
|
|
if (borderHsl) {
|
|
|
|
|
const borderHex = hslToHex(...borderHsl);
|
|
|
|
|
document.documentElement.style.setProperty("--border", borderHex);
|
|
|
|
|
}
|
2026-02-15 21:33:18 +00:00
|
|
|
|
2026-02-15 21:53:50 +00:00
|
|
|
// Update ALL color-related CSS variables to match the theme
|
|
|
|
|
// This overrides any bot config colors
|
2026-02-15 23:19:16 +00:00
|
|
|
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})`);
|
2026-02-15 21:33:18 +00:00
|
|
|
|
2026-02-15 23:19:16 +00:00
|
|
|
console.log("✓ Theme colors applied:", { bg: background, primary: primary });
|
2026-02-15 21:53:50 +00:00
|
|
|
updateDropdown();
|
|
|
|
|
subscribers.forEach((cb) => cb({ themeId: id, themeName: theme.name }));
|
|
|
|
|
}, 50);
|
2025-12-03 18:42:22 -03:00
|
|
|
};
|
|
|
|
|
link.onerror = () => console.error("✗ Failed:", theme.name);
|
|
|
|
|
document.head.appendChild(link);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateDropdown() {
|
|
|
|
|
const dd = document.getElementById("themeDropdown");
|
|
|
|
|
if (dd) dd.value = currentThemeId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createDropdown() {
|
|
|
|
|
const select = document.createElement("select");
|
|
|
|
|
select.id = "themeDropdown";
|
|
|
|
|
select.className = "theme-dropdown";
|
2026-01-11 20:58:11 -03:00
|
|
|
themes.forEach((t) => {
|
2025-12-03 18:42:22 -03:00
|
|
|
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() {
|
2026-02-15 23:19:16 +00:00
|
|
|
// Ensure data-theme is set on html element so CSS selectors work
|
|
|
|
|
if (!document.documentElement.getAttribute("data-theme")) {
|
|
|
|
|
document.documentElement.setAttribute("data-theme", "sentient");
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 13:54:16 +00:00
|
|
|
// 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
|
|
|
|
|
let saved = localStorage.getItem("gb-theme");
|
|
|
|
|
if (!saved || !themes.find((t) => t.id === saved)) {
|
|
|
|
|
// No user preference, try bot-specific theme
|
|
|
|
|
const botId = getCurrentBotId();
|
|
|
|
|
saved = botThemeMap[botId] || "light";
|
|
|
|
|
// Save to localStorage so it persists
|
|
|
|
|
localStorage.setItem("gb-theme", saved);
|
|
|
|
|
}
|
2026-01-11 20:58:11 -03:00
|
|
|
if (!themes.find((t) => t.id === saved)) saved = "default";
|
2025-12-03 18:42:22 -03:00
|
|
|
currentThemeId = saved;
|
|
|
|
|
loadTheme(saved);
|
2026-01-11 20:58:11 -03:00
|
|
|
|
2025-12-03 18:42:22 -03:00
|
|
|
const container = document.getElementById("themeSelectorContainer");
|
|
|
|
|
if (container) container.appendChild(createDropdown());
|
2026-01-11 20:58:11 -03:00
|
|
|
|
2025-12-03 18:42:22 -03:00
|
|
|
console.log("✓ Theme Manager initialized");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setThemeFromServer(data) {
|
2026-02-10 13:54:16 +00:00
|
|
|
// Save theme to localStorage for persistence across page loads
|
|
|
|
|
localStorage.setItem("gb-theme-data", JSON.stringify(data));
|
|
|
|
|
|
|
|
|
|
// Load base theme if specified
|
|
|
|
|
if (data.theme_base) {
|
|
|
|
|
loadTheme(data.theme_base);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 18:42:22 -03:00
|
|
|
if (data.logo_url) {
|
2026-02-13 22:31:49 +00:00
|
|
|
// 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
|
2026-01-11 20:58:11 -03:00
|
|
|
document
|
2026-02-13 22:31:49 +00:00
|
|
|
.querySelectorAll(".assistant-avatar")
|
2026-01-11 20:58:11 -03:00
|
|
|
.forEach((el) => {
|
|
|
|
|
el.style.backgroundImage = `url("${data.logo_url}")`;
|
2026-02-10 13:54:16 +00:00
|
|
|
el.style.backgroundSize = "contain";
|
|
|
|
|
el.style.backgroundRepeat = "no-repeat";
|
|
|
|
|
el.style.backgroundPosition = "center";
|
2026-01-11 20:58:11 -03:00
|
|
|
});
|
2025-12-03 18:42:22 -03:00
|
|
|
}
|
2026-02-10 13:54:16 +00:00
|
|
|
if (data.color1) {
|
|
|
|
|
document.documentElement.style.setProperty("--color1", data.color1);
|
|
|
|
|
}
|
|
|
|
|
if (data.color2) {
|
|
|
|
|
document.documentElement.style.setProperty("--color2", data.color2);
|
|
|
|
|
}
|
2025-12-03 18:42:22 -03:00
|
|
|
if (data.title) document.title = data.title;
|
|
|
|
|
if (data.logo_text) {
|
2026-02-10 13:54:16 +00:00
|
|
|
document.querySelectorAll(".logo span, .logo-text").forEach((el) => {
|
2025-12-03 18:42:22 -03:00
|
|
|
el.textContent = data.logo_text;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 13:54:16 +00:00
|
|
|
// Load saved theme from localStorage on page load
|
|
|
|
|
function loadSavedTheme() {
|
|
|
|
|
const savedTheme = localStorage.getItem("gb-theme-data");
|
|
|
|
|
if (savedTheme) {
|
|
|
|
|
try {
|
|
|
|
|
const data = JSON.parse(savedTheme);
|
|
|
|
|
setThemeFromServer(data);
|
|
|
|
|
console.log("✓ Theme loaded from localStorage");
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn("Failed to load saved theme:", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 18:42:22 -03:00
|
|
|
function applyCustomizations() {
|
|
|
|
|
// Called by modules if needed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function subscribe(cb) {
|
|
|
|
|
subscribers.push(cb);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 20:58:11 -03:00
|
|
|
return {
|
|
|
|
|
init,
|
|
|
|
|
loadTheme,
|
|
|
|
|
setThemeFromServer,
|
2026-02-10 13:54:16 +00:00
|
|
|
loadSavedTheme,
|
2026-01-11 20:58:11 -03:00
|
|
|
applyCustomizations,
|
|
|
|
|
subscribe,
|
|
|
|
|
getAvailableThemes: () => themes,
|
|
|
|
|
};
|
2025-12-03 18:42:22 -03:00
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
window.ThemeManager = ThemeManager;
|