// Theme registry // --------------- // Discovers themes from frontend/themes// via the backend // /api/themes/list endpoint, s each one's CSS into , and // exposes the list at window.ThemeRegistry for the topbar dropdown. // // Every theme — built-in or custom — lives in frontend/themes//. // The inline bootstrap in index.html already s the saved theme // synchronously before first paint; this script s the rest so the // dropdown can switch between them without another network round-trip. // // If the API fails completely, the dropdown falls back to a single // option (the currently-loaded theme from data-theme). The user can't // switch, but the page still renders correctly. (function () { var currentTheme = document.documentElement.getAttribute('data-theme') || 'nebula'; var state = { themes: [{ name: currentTheme, displayName: currentTheme, css: '/themes/' + currentTheme + '/theme.css', builtin: false, }], loaded: false, listeners: [], }; function notify() { state.listeners.forEach(function (cb) { try { cb(state.themes); } catch (_) { /* swallow */ } }); } function linkThemeCss(theme) { // The inline bootstrap already linked the current theme; this dedupes. if (document.head.querySelector('link[data-theme-css="' + theme.name + '"]')) return; var link = document.createElement('link'); link.rel = 'stylesheet'; link.href = theme.css; link.setAttribute('data-theme-css', theme.name); document.head.appendChild(link); } function fetchThemes() { return fetch('/api/themes/list', { credentials: 'same-origin' }) .then(function (r) { return r.ok ? r.json() : []; }) .then(function (list) { if (Array.isArray(list) && list.length) state.themes = list; state.themes.forEach(linkThemeCss); state.loaded = true; notify(); }) .catch(function () { // Network/API failure — keep the built-in fallback list. state.loaded = true; notify(); }); } window.ThemeRegistry = { /* Synchronous accessor — returns whatever list we have, even if the fetch is still in flight. Always includes at least the built-ins. */ list: function () { return state.themes.slice(); }, /* True once /api/themes/list has resolved (success or failure). */ isLoaded: function () { return state.loaded; }, /* Subscribe to list updates. Called immediately with the current list and again after the fetch resolves. */ onChange: function (cb) { state.listeners.push(cb); try { cb(state.themes); } catch (_) { /* swallow */ } }, /* Manually trigger a refresh — used after the user drops a new theme folder and hits "reload themes" (no UI for that yet). */ refresh: fetchThemes, }; fetchThemes(); })();