mirror of
https://github.com/infinition/Bjorn.git
synced 2026-03-10 14:42:04 +00:00
- Implemented methods for fetching AI stats, training history, and recent experiences. - Added functionality to set operation mode (MANUAL, AUTO, AI) with appropriate handling. - Included helper methods for querying the database and sending JSON responses. - Integrated model metadata extraction for visualization purposes.
279 lines
7.7 KiB
JavaScript
279 lines
7.7 KiB
JavaScript
/**
|
|
* Theme module — CSS variable management, persistence, theme editor UI.
|
|
* Single source of truth: all colors come from :root CSS variables.
|
|
*
|
|
* Supports:
|
|
* - Preset themes (default Nordic Acid, light, etc.)
|
|
* - User custom overrides persisted to localStorage
|
|
* - Theme editor with color pickers + raw CSS textarea
|
|
* - Icon pack switching via icon registry
|
|
*/
|
|
|
|
import { t } from './i18n.js';
|
|
|
|
const STORAGE_KEY = 'bjorn_theme';
|
|
const ICON_PACK_KEY = 'bjorn_icon_pack';
|
|
|
|
/* Default theme tokens — matches global.css :root */
|
|
const DEFAULT_THEME = {
|
|
'--bg': '#050709',
|
|
'--bg-2': '#0b0f14',
|
|
'--ink': '#e6fff7',
|
|
'--muted': '#8affc1cc',
|
|
'--acid': '#00ff9a',
|
|
'--acid-2': '#18f0ff',
|
|
'--danger': '#ff3b3b',
|
|
'--warning': '#ffd166',
|
|
'--ok': '#2cff7e',
|
|
'--accent': '#22f0b4',
|
|
'--accent-2': '#18d6ff',
|
|
'--c-border': '#00ffff22',
|
|
'--c-border-strong': '#00ffff33',
|
|
'--panel': '#0e1717',
|
|
'--panel-2': '#101c1c',
|
|
'--c-panel': '#0b1218',
|
|
'--radius': '14px'
|
|
};
|
|
|
|
/* Editable token groups for the theme editor */
|
|
const TOKEN_GROUPS = [
|
|
{
|
|
label: 'theme.group.colors',
|
|
tokens: [
|
|
{ key: '--bg', label: 'theme.token.bg', type: 'color' },
|
|
{ key: '--ink', label: 'theme.token.ink', type: 'color' },
|
|
{ key: '--acid', label: 'theme.token.accent1', type: 'color' },
|
|
{ key: '--acid-2', label: 'theme.token.accent2', type: 'color' },
|
|
{ key: '--danger', label: 'theme.token.danger', type: 'color' },
|
|
{ key: '--warning', label: 'theme.token.warning', type: 'color' },
|
|
{ key: '--ok', label: 'theme.token.ok', type: 'color' },
|
|
]
|
|
},
|
|
{
|
|
label: 'theme.group.surfaces',
|
|
tokens: [
|
|
{ key: '--panel', label: 'theme.token.panel', type: 'color' },
|
|
{ key: '--panel-2', label: 'theme.token.panel2', type: 'color' },
|
|
{ key: '--c-panel', label: 'theme.token.ctrlPanel', type: 'color' },
|
|
{ key: '--c-border', label: 'theme.token.border', type: 'color' },
|
|
]
|
|
},
|
|
{
|
|
label: 'theme.group.layout',
|
|
tokens: [
|
|
{ key: '--radius', label: 'theme.token.radius', type: 'text' },
|
|
]
|
|
}
|
|
];
|
|
|
|
let _userOverrides = {};
|
|
|
|
/* -- Icon registry -- */
|
|
const _iconPacks = {
|
|
default: {} // populated from /web/images/*.png
|
|
};
|
|
let _currentPack = 'default';
|
|
|
|
/* Load user theme from localStorage */
|
|
function loadSaved() {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
if (raw) _userOverrides = JSON.parse(raw);
|
|
} catch { _userOverrides = {}; }
|
|
|
|
try {
|
|
_currentPack = localStorage.getItem(ICON_PACK_KEY) || 'default';
|
|
} catch { _currentPack = 'default'; }
|
|
}
|
|
|
|
/* Apply overrides to :root */
|
|
function applyToDOM() {
|
|
const root = document.documentElement;
|
|
// Reset to defaults first
|
|
for (const [k, v] of Object.entries(DEFAULT_THEME)) {
|
|
root.style.setProperty(k, v);
|
|
}
|
|
// Apply user overrides on top
|
|
for (const [k, v] of Object.entries(_userOverrides)) {
|
|
if (v) root.style.setProperty(k, v);
|
|
}
|
|
}
|
|
|
|
/* Save overrides to localStorage */
|
|
function persist() {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(_userOverrides));
|
|
} catch { /* storage full or blocked */ }
|
|
}
|
|
|
|
/* -- Public API -- */
|
|
|
|
export function init() {
|
|
loadSaved();
|
|
applyToDOM();
|
|
}
|
|
|
|
/** Get current value for a token */
|
|
export function getToken(key) {
|
|
return _userOverrides[key] || DEFAULT_THEME[key] || '';
|
|
}
|
|
|
|
/** Set a single token override */
|
|
export function setToken(key, value) {
|
|
_userOverrides[key] = value;
|
|
document.documentElement.style.setProperty(key, value);
|
|
persist();
|
|
}
|
|
|
|
/** Reset all overrides to default */
|
|
export function resetToDefault() {
|
|
_userOverrides = {};
|
|
persist();
|
|
applyToDOM();
|
|
}
|
|
|
|
/** Apply a full theme preset */
|
|
export function applyPreset(preset) {
|
|
_userOverrides = { ...preset };
|
|
persist();
|
|
applyToDOM();
|
|
}
|
|
|
|
/** Get current overrides (for display in editor) */
|
|
export function getCurrentOverrides() {
|
|
return { ...DEFAULT_THEME, ..._userOverrides };
|
|
}
|
|
|
|
/* -- Icon registry -- */
|
|
|
|
/**
|
|
* Register an icon pack.
|
|
* @param {string} name
|
|
* @param {object} icons - { logicalName: svgString | url }
|
|
*/
|
|
export function registerIconPack(name, icons) {
|
|
_iconPacks[name] = icons;
|
|
}
|
|
|
|
/** Get an icon by logical name from current pack */
|
|
export function icon(name) {
|
|
const pack = _iconPacks[_currentPack] || _iconPacks.default;
|
|
return pack[name] || _iconPacks.default[name] || '';
|
|
}
|
|
|
|
/** Switch icon pack */
|
|
export function setIconPack(name) {
|
|
if (!_iconPacks[name]) {
|
|
console.warn(`[Theme] Unknown icon pack: ${name}`);
|
|
return;
|
|
}
|
|
_currentPack = name;
|
|
try { localStorage.setItem(ICON_PACK_KEY, name); } catch { /* */ }
|
|
}
|
|
|
|
/* -- Theme Editor UI -- */
|
|
|
|
/**
|
|
* Mount the theme editor into a container element.
|
|
* @param {HTMLElement} container
|
|
*/
|
|
export function mountEditor(container) {
|
|
container.innerHTML = '';
|
|
|
|
const current = getCurrentOverrides();
|
|
|
|
// Color pickers grouped
|
|
for (const group of TOKEN_GROUPS) {
|
|
const section = document.createElement('div');
|
|
section.className = 'theme-group';
|
|
|
|
const heading = document.createElement('h4');
|
|
heading.className = 'theme-group-title';
|
|
heading.textContent = t(group.label);
|
|
section.appendChild(heading);
|
|
|
|
for (const token of group.tokens) {
|
|
const row = document.createElement('div');
|
|
row.className = 'theme-row';
|
|
|
|
const label = document.createElement('label');
|
|
label.textContent = t(token.label);
|
|
label.className = 'theme-label';
|
|
|
|
const input = document.createElement('input');
|
|
input.type = token.type === 'color' ? 'color' : 'text';
|
|
input.className = 'theme-input';
|
|
input.value = normalizeColor(current[token.key] || '');
|
|
input.addEventListener('input', () => {
|
|
setToken(token.key, input.value);
|
|
});
|
|
|
|
row.appendChild(label);
|
|
row.appendChild(input);
|
|
section.appendChild(row);
|
|
}
|
|
|
|
container.appendChild(section);
|
|
}
|
|
|
|
// Raw CSS textarea (advanced)
|
|
const advSection = document.createElement('div');
|
|
advSection.className = 'theme-group';
|
|
|
|
const advTitle = document.createElement('h4');
|
|
advTitle.className = 'theme-group-title';
|
|
advTitle.textContent = t('theme.advanced');
|
|
advSection.appendChild(advTitle);
|
|
|
|
const textarea = document.createElement('textarea');
|
|
textarea.className = 'theme-raw-css';
|
|
textarea.rows = 6;
|
|
textarea.placeholder = '--my-var: #ff0000;\n--other: 12px;';
|
|
textarea.value = Object.entries(_userOverrides)
|
|
.filter(([k]) => !TOKEN_GROUPS.some(g => g.tokens.some(tk => tk.key === k)))
|
|
.map(([k, v]) => `${k}: ${v};`)
|
|
.join('\n');
|
|
advSection.appendChild(textarea);
|
|
|
|
const applyBtn = document.createElement('button');
|
|
applyBtn.className = 'btn btn-sm';
|
|
applyBtn.textContent = t('theme.applyRaw');
|
|
applyBtn.addEventListener('click', () => {
|
|
parseAndApplyRawCSS(textarea.value);
|
|
});
|
|
advSection.appendChild(applyBtn);
|
|
|
|
// Reset button
|
|
const resetBtn = document.createElement('button');
|
|
resetBtn.className = 'btn btn-sm btn-danger';
|
|
resetBtn.textContent = t('theme.reset');
|
|
resetBtn.addEventListener('click', () => {
|
|
resetToDefault();
|
|
mountEditor(container); // Re-render editor
|
|
});
|
|
advSection.appendChild(resetBtn);
|
|
|
|
container.appendChild(advSection);
|
|
}
|
|
|
|
/** Parse raw CSS var declarations from textarea */
|
|
function parseAndApplyRawCSS(raw) {
|
|
const lines = raw.split('\n');
|
|
for (const line of lines) {
|
|
const match = line.match(/^\s*(--[\w-]+)\s*:\s*(.+?)\s*;?\s*$/);
|
|
if (match) {
|
|
setToken(match[1], match[2]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Normalize a CSS color to #hex for color picker inputs */
|
|
function normalizeColor(val) {
|
|
if (!val || val.includes('var(') || val.includes('rgba') || val.includes('color-mix')) {
|
|
return val; // Can't normalize complex values
|
|
}
|
|
// If it's already a hex, return as-is (truncate alpha channel for color picker)
|
|
if (/^#[0-9a-f]{6,8}$/i.test(val)) return val.slice(0, 7);
|
|
return val;
|
|
}
|