var builtinThemeManifests = [
{
id: 'ai-purple',
name: 'AI Purple (Default)',
description: 'The original Roboticus control-room palette with cool blues, violet accents, and a crisp operator feel.',
author: 'Roboticus',
source: 'builtin',
swatch: '#c180ff',
variables: {
'--bg': '#060e20',
'--surface': '#081329',
'--surface-2': '#0c1934',
'--accent': '#c180ff',
'--text': '#dee5ff',
'--muted': '#9baad6',
'--border': '#38476d'
}
},
{
id: 'crt-orange',
name: 'CRT Orange',
description: 'A warm phosphor console look with amber text, dark glass panels, and bunker-terminal energy.',
author: 'Roboticus',
source: 'builtin',
swatch: '#ff8c00',
variables: {
'--bg': '#120b04',
'--surface': '#1f1307',
'--surface-2': '#2b1a09',
'--accent': '#ff8c00',
'--text': '#ffd9a8',
'--muted': '#c9a26d',
'--border': '#6e4720'
}
},
{
id: 'crt-green',
name: 'CRT Green',
description: 'Classic monochrome terminal phosphor with green glow, dense contrast, and old-school ops-room restraint.',
author: 'Roboticus',
source: 'builtin',
swatch: '#00ff41',
variables: {
'--bg': '#040d06',
'--surface': '#07170a',
'--surface-2': '#0c2110',
'--accent': '#00ff41',
'--text': '#bbffcb',
'--muted': '#71c786',
'--border': '#1d6a2f'
}
},
{
id: 'psychedelic',
name: 'Psychedelic Freakout',
description: 'A maximalist neon chaos mode with saturated gradients and unmistakably unserious operator vibes.',
author: 'Roboticus',
source: 'builtin',
swatch: 'linear-gradient(135deg,#ff00ff,#00ffcc,#ffff00)',
variables: {
'--bg': '#14061d',
'--surface': '#270d38',
'--surface-2': '#3a1553',
'--accent': '#ff4ff8',
'--text': '#fff4a8',
'--muted': '#8fffe5',
'--border': '#ff8ef2'
}
}
];
var themeNames = {};
var loadedThemes = {}; var themeStyleEl = null; var themeFontEls = {};
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
document.querySelectorAll('.theme-option').forEach(function(o) { o.classList.toggle('active', o.getAttribute('data-theme-set') === theme); });
var nameEl = document.getElementById('theme-name'); if (nameEl) nameEl.textContent = themeNames[theme] || theme;
applyTextures(theme);
var ext = loadedThemes[theme];
if (ext && ext.fonts && ext.fonts.length > 0 && !themeFontEls[theme]) {
ext.fonts.forEach(function(url) {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = url;
document.head.appendChild(link);
});
themeFontEls[theme] = true;
}
}
function applyTextures(themeId) {
document.querySelectorAll('.theme-texture-overlay').forEach(function(el) { el.remove(); });
document.body.style.removeProperty('background-image');
var ext = loadedThemes[themeId];
if (!ext || !ext.textures) return;
if (ext.textures.body && ext.textures.body.type === 'css') {
document.body.style.backgroundImage = ext.textures.body.value;
}
if (ext.textures.surface && ext.textures.surface.type === 'css') {
var style = document.getElementById('theme-texture-css');
if (!style) { style = document.createElement('style'); style.id = 'theme-texture-css'; document.head.appendChild(style); }
style.textContent = '[data-theme="' + themeId + '"] .card, [data-theme="' + themeId + '"] .surface { background-image: ' + ext.textures.surface.value + '; }';
} else {
var existing = document.getElementById('theme-texture-css');
if (existing) existing.textContent = '';
}
}
function injectExternalThemeCSS(manifest) {
if (!themeStyleEl) {
themeStyleEl = document.createElement('style');
themeStyleEl.id = 'external-themes-css';
document.head.appendChild(themeStyleEl);
}
var css = '[data-theme="' + manifest.id + '"] {\n';
Object.keys(manifest.variables).forEach(function(key) {
css += ' ' + key + ': ' + manifest.variables[key] + ';\n';
});
css += '}\n';
themeStyleEl.textContent += css;
}
function buildThemePreview(vars) {
var bg = vars['--bg'] || '#060e20';
var surface = vars['--surface'] || '#081329';
var surface2 = vars['--surface-2'] || '#0c1934';
var accent = vars['--accent'] || '#c180ff';
var text = vars['--text'] || '#dee5ff';
var muted = vars['--muted'] || '#9baad6';
var border = vars['--border'] || '#38476d';
var div = document.createElement('div');
div.className = 'theme-preview';
var inner = document.createElement('div');
inner.className = 'theme-preview-inner';
inner.style.background = bg;
var header = document.createElement('div');
header.className = 'theme-preview-header';
header.style.cssText = 'background:' + surface + ';border-bottom:1px solid ' + border;
[accent, muted, muted].forEach(function(c) {
var dot = document.createElement('div');
dot.className = 'theme-preview-dot';
dot.style.background = c;
header.appendChild(dot);
});
inner.appendChild(header);
var body = document.createElement('div');
body.className = 'theme-preview-body';
var sidebar = document.createElement('div');
sidebar.className = 'theme-preview-sidebar';
sidebar.style.background = surface;
body.appendChild(sidebar);
var content = document.createElement('div');
content.className = 'theme-preview-content';
content.style.background = surface2;
[[text, '70%', 0.6], [muted, '50%', 0.4], [accent, '35%', 0.8]].forEach(function(l) {
var line = document.createElement('div');
line.className = 'theme-preview-line';
line.style.cssText = 'background:' + l[0] + ';width:' + l[1] + ';opacity:' + l[2];
content.appendChild(line);
});
body.appendChild(content);
inner.appendChild(body);
div.appendChild(inner);
return div;
}
function addThemeToDropdown(manifest) {
var dropdown = document.getElementById('theme-dropdown');
if (!dropdown) return;
if (dropdown.querySelector('[data-theme-set="' + manifest.id + '"]')) return;
var btn = document.createElement('button');
btn.className = 'theme-option';
btn.setAttribute('data-theme-set', manifest.id);
var vars = manifest.variables || {};
if (manifest.thumbnail) {
var thumb = document.createElement('div');
thumb.className = 'theme-preview';
thumb.style.backgroundImage = 'url(' + manifest.thumbnail + ')';
thumb.style.backgroundSize = 'cover';
thumb.style.backgroundPosition = 'center';
btn.appendChild(thumb);
} else if (!vars['--bg']) {
var simple = document.createElement('div');
simple.className = 'theme-preview';
simple.style.background = manifest.swatch || vars['--accent'] || '#888';
btn.appendChild(simple);
} else {
btn.appendChild(buildThemePreview(vars));
}
var info = document.createElement('div');
info.className = 'theme-info';
var nameEl = document.createElement('div');
nameEl.className = 'theme-info-name';
nameEl.textContent = manifest.name;
info.appendChild(nameEl);
if (manifest.description) {
var desc = document.createElement('div');
desc.className = 'theme-info-desc';
desc.textContent = manifest.description;
info.appendChild(desc);
}
if (manifest.author || manifest.source) {
var meta = document.createElement('div');
meta.className = 'theme-info-meta';
if (manifest.author) meta.textContent = 'by ' + manifest.author;
if (manifest.source) {
meta.textContent += (meta.textContent ? ' \u00b7 ' : '') + manifest.source;
}
info.appendChild(meta);
}
btn.appendChild(info);
dropdown.appendChild(btn);
}
function registerThemeManifest(manifest, options) {
options = options || {};
loadedThemes[manifest.id] = manifest;
themeNames[manifest.id] = manifest.name;
if (options.injectCss) injectExternalThemeCSS(manifest);
addThemeToDropdown(manifest);
}
builtinThemeManifests.forEach(function(theme) {
registerThemeManifest(theme, { injectCss: false });
});
function loadExternalThemes() {
fetch('/api/themes').then(function(r) { return r.json(); }).then(function(themes) {
themes.forEach(function(t) {
if (themeNames[t.id]) return;
registerThemeManifest(t, { injectCss: true });
});
var saved = localStorage.getItem('roboticus-theme');
if (saved && loadedThemes[saved]) applyTheme(saved);
}).catch(function(e) {
console.warn('Failed to load external themes:', e);
});
}
var catalogCache = null;
function loadCatalog(cb) {
if (catalogCache) return cb(catalogCache);
fetch('/api/themes/catalog').then(function(r) { return r.json(); }).then(function(data) {
catalogCache = data.catalog || [];
cb(catalogCache);
}).catch(function(e) {
console.warn('Failed to load theme catalog:', e);
cb([]);
});
}
function buildCatalogModalDom() {
var overlay = document.createElement('div');
overlay.id = 'theme-catalog-overlay';
overlay.className = 'theme-catalog-overlay';
var modal = document.createElement('div');
modal.className = 'theme-catalog-modal';
var header = document.createElement('div');
header.className = 'theme-catalog-header';
var h3 = document.createElement('h3');
h3.textContent = 'Theme Catalog';
header.appendChild(h3);
var closeBtn = document.createElement('button');
closeBtn.className = 'theme-catalog-close';
closeBtn.title = 'Close';
closeBtn.textContent = '\u00d7';
closeBtn.addEventListener('click', closeCatalogModal);
header.appendChild(closeBtn);
modal.appendChild(header);
var grid = document.createElement('div');
grid.className = 'theme-catalog-grid';
grid.id = 'theme-catalog-grid';
modal.appendChild(grid);
overlay.appendChild(modal);
overlay.addEventListener('click', function(e) { if (e.target === overlay) closeCatalogModal(); });
document.body.appendChild(overlay);
return overlay;
}
function buildCatalogCard(entry) {
var card = document.createElement('div');
card.className = 'theme-catalog-card';
var swatch = document.createElement('div');
swatch.className = 'theme-catalog-swatch';
swatch.style.background = entry.preview_swatch || entry.manifest.swatch || 'var(--surface-2)';
card.appendChild(swatch);
var info = document.createElement('div');
info.className = 'theme-catalog-card-info';
var name = document.createElement('div');
name.className = 'theme-catalog-card-name';
name.textContent = entry.manifest.name || entry.name || entry.manifest.id;
info.appendChild(name);
var desc = document.createElement('div');
desc.className = 'theme-catalog-card-desc';
desc.textContent = entry.manifest.description || entry.description || '';
info.appendChild(desc);
var meta = document.createElement('div');
meta.className = 'theme-catalog-card-meta';
meta.textContent = 'v' + (entry.version || '1.0.0') + ' by ' + (entry.manifest.author || entry.author || 'unknown');
info.appendChild(meta);
card.appendChild(info);
var btn = document.createElement('button');
btn.className = 'theme-catalog-install-btn';
var isInstalled = entry.installed || !!themeNames[entry.manifest.id || entry.id];
if (isInstalled) {
btn.textContent = 'Installed';
btn.classList.add('installed');
} else {
btn.textContent = 'Install';
btn.addEventListener('click', function() {
var themeId = entry.manifest.id || entry.id;
btn.textContent = '...';
btn.disabled = true;
fetch('/api/themes/catalog/install', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: themeId })
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.ok && data.theme) {
registerThemeManifest(data.theme, { injectCss: true });
btn.textContent = 'Installed';
btn.classList.add('installed');
btn.disabled = false;
entry.installed = true;
} else {
btn.textContent = data.error || 'Error';
btn.disabled = false;
}
}).catch(function() {
btn.textContent = 'Error';
btn.disabled = false;
});
});
}
card.appendChild(btn);
return card;
}
function openCatalogModal() {
var overlay = document.getElementById('theme-catalog-overlay');
if (!overlay) overlay = buildCatalogModalDom();
var grid = document.getElementById('theme-catalog-grid');
grid.textContent = '';
var loading = document.createElement('div');
loading.style.cssText = 'text-align:center;padding:2rem;color:var(--muted);font-size:0.75rem;';
loading.textContent = 'Loading catalog...';
grid.appendChild(loading);
overlay.classList.add('open');
loadCatalog(function(catalog) {
grid.textContent = '';
if (!catalog.length) {
var empty = document.createElement('div');
empty.style.cssText = 'text-align:center;padding:2rem;color:var(--muted);font-size:0.75rem;';
empty.textContent = 'No catalog themes available.';
grid.appendChild(empty);
return;
}
catalog.forEach(function(entry) {
grid.appendChild(buildCatalogCard(entry));
});
});
}
function closeCatalogModal() {
var overlay = document.getElementById('theme-catalog-overlay');
if (overlay) overlay.classList.remove('open');
}
var savedTheme = localStorage.getItem('roboticus-theme');
if (savedTheme && themeNames[savedTheme]) applyTheme(savedTheme);
loadExternalThemes();
var themeToggle = document.getElementById('theme-toggle');
var themeDropdown = document.getElementById('theme-dropdown');
if (themeToggle && themeDropdown) {
themeToggle.addEventListener('click', function(e) { e.stopPropagation(); themeDropdown.classList.toggle('open'); });
document.addEventListener('click', function(e) {
if (!e.target.closest('#theme-catalog-overlay')) themeDropdown.classList.remove('open');
});
themeDropdown.addEventListener('click', function(e) {
var opt = e.target.closest('[data-theme-set]'); if (!opt) return;
var theme = opt.getAttribute('data-theme-set');
applyTheme(theme);
localStorage.setItem('roboticus-theme', theme);
themeDropdown.classList.remove('open');
if (App.page === 'overview') App.navigate('overview');
});
var browseBtn = document.createElement('button');
browseBtn.className = 'theme-catalog-browse-btn';
browseBtn.textContent = '+ Browse catalog';
browseBtn.addEventListener('click', function(e) {
e.stopPropagation();
themeDropdown.classList.remove('open');
openCatalogModal();
});
themeDropdown.appendChild(browseBtn);
}