botui/ui/suite/js/theme-manager.js

404 lines
14 KiB
JavaScript
Raw Normal View History

2025-12-03 18:42:22 -03:00
// 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";
}
2025-12-03 18:42:22 -03:00
const themes = [
{ 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" },
{
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" },
{ id: "xtreegold", name: "📁 XTree", file: "xtreegold.css" },
2025-12-03 18:42:22 -03:00
];
function loadTheme(id) {
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";
2026-02-20 01:14:14 +00:00
const botId = getCurrentBotId();
localStorage.setItem(`gb-theme-${botId}`, "default");
// 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";
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;
2026-02-20 01:14:14 +00:00
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,
);
}
2026-02-20 01:14:14 +00:00
// 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})`,
);
2026-02-20 01:14:14 +00:00
}
console.log("✓ Theme colors applied:", {
bg: background,
primary: primary,
});
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 select = document.getElementById("themeDropdown");
if (select) select.value = currentThemeId;
2025-12-03 18:42:22 -03:00
}
function createDropdown() {
const select = document.createElement("select");
select.id = "themeDropdown";
select.className = "theme-dropdown";
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() {
// 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
2026-02-20 01:14:14 +00:00
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
2026-02-20 01:14:14 +00:00
localStorage.setItem(`gb-theme-${botId}`, saved);
}
if (!themes.find((t) => t.id === saved)) saved = "default";
2025-12-03 18:42:22 -03:00
currentThemeId = saved;
loadTheme(saved);
// Dropdown injection restored for the window manager
const container = document.getElementById("themeSelectorContainer");
if (container) {
container.innerHTML = "";
container.appendChild(createDropdown());
}
2025-12-03 18:42:22 -03:00
console.log("✓ Theme Manager initialized");
}
function setThemeFromServer(data) {
// Save theme to localStorage for persistence across page loads
2026-02-20 01:14:14 +00:00
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);
}
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");
2026-02-13 22:31:49 +00:00
if (logoImg && logoSvg) {
logoImg.src = data.logo_url;
logoImg.alt = data.title || "Logo";
logoImg.style.display = "block";
logoSvg.style.display = "none";
2026-02-13 22:31:49 +00:00
}
// 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";
});
2025-12-03 18:42:22 -03: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) {
document.querySelectorAll(".logo span, .logo-text").forEach((el) => {
2025-12-03 18:42:22 -03:00
el.textContent = data.logo_text;
});
}
}
// Load saved theme from localStorage on page load
function loadSavedTheme() {
2026-02-20 01:14:14 +00:00
const botId = getCurrentBotId();
const savedTheme = localStorage.getItem(`gb-theme-data-${botId}`);
if (savedTheme) {
try {
const data = JSON.parse(savedTheme);
setThemeFromServer(data);
2026-02-20 01:14:14 +00:00
console.log(`✓ Theme loaded from localStorage for ${botId}`);
} 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);
}
return {
init,
loadTheme,
setThemeFromServer,
loadSavedTheme,
applyCustomizations,
subscribe,
getAvailableThemes: () => themes,
};
2025-12-03 18:42:22 -03:00
})();
window.ThemeManager = ThemeManager;