aboutsummaryrefslogtreecommitdiff
path: root/modules/ext/theme.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/ext/theme.js')
-rw-r--r--modules/ext/theme.js172
1 files changed, 172 insertions, 0 deletions
diff --git a/modules/ext/theme.js b/modules/ext/theme.js
new file mode 100644
index 0000000..9a1dab5
--- /dev/null
+++ b/modules/ext/theme.js
@@ -0,0 +1,172 @@
+class ThemeModule {
+ constructor() {
+ this.commands = {};
+ this.terminal = null;
+ this.key = 'webx11_themes';
+ this._previewTimer = null;
+ this.defaults = {
+ 'dark': { '--bg-color': '#000000', '--bg-secondary': '#0a0a0a', '--bg-tertiary': '#111111', '--text-color': '#00ff00', '--text-muted': '#888888', '--accent-color': '#00aa00', '--border-color': '#005500' },
+ 'light': { '--bg-color': '#ffffff', '--bg-secondary': '#f5f5f5', '--bg-tertiary': '#eeeeee', '--text-color': '#222222', '--text-muted': '#888888', '--accent-color': '#0066cc', '--border-color': '#cccccc' },
+ 'amber': { '--bg-color': '#1a0f00', '--bg-secondary': '#2a1a00', '--bg-tertiary': '#332200', '--text-color': '#ffaa00', '--text-muted': '#886600', '--accent-color': '#ff8800', '--border-color': '#663300' },
+ 'cyber': { '--bg-color': '#000a14', '--bg-secondary': '#001a2e', '--bg-tertiary': '#002240', '--text-color': '#00ffff', '--text-muted': '#0088aa', '--accent-color': '#ff00ff', '--border-color': '#004466' },
+ 'ocean': { '--bg-color': '#0a1628', '--bg-secondary': '#0f2240', '--bg-tertiary': '#142e54', '--text-color': '#88ddff', '--text-muted': '#5599bb', '--accent-color': '#00aaff', '--border-color': '#1a4470' },
+ 'forest': { '--bg-color': '#0a1a0e', '--bg-secondary': '#0f2814', '--bg-tertiary': '#14361c', '--text-color': '#88dd88', '--text-muted': '#559955', '--accent-color': '#33cc33', '--border-color': '#1a4a22' },
+ 'monochrome': { '--bg-color': '#0a0a0a', '--bg-secondary': '#181818', '--bg-tertiary': '#222222', '--text-color': '#cccccc', '--text-muted': '#666666', '--accent-color': '#999999', '--border-color': '#333333' },
+ 'sunset': { '--bg-color': '#1a0a14', '--bg-secondary': '#2a1220', '--bg-tertiary': '#3a1a2c', '--text-color': '#ffbb99', '--text-muted': '#cc7755', '--accent-color': '#ff6644', '--border-color': '#552233' },
+ 'midnight': { '--bg-color': '#050510', '--bg-secondary': '#0a0a20', '--bg-tertiary': '#101030', '--text-color': '#aaaaee', '--text-muted': '#555588', '--accent-color': '#6666ff', '--border-color': '#1a1a3a' }
+ };
+ this.themes = this.load();
+ }
+ requirements() {
+ return { met: true, missing: [] };
+ }
+ getHelpInfo() {
+ return { 'theme': 'theme <set|list|new|delete|export|preview|keep> [...] — Manage and preview visual themes' };
+ }
+ load() {
+ try {
+ const data = localStorage.getItem(this.key);
+ return data ? { ...this.defaults, ...JSON.parse(data) } : { ...this.defaults };
+ } catch (e) { return { ...this.defaults }; }
+ }
+ save() {
+ const custom = {};
+ Object.keys(this.themes).forEach(k => {
+ if (!this.defaults[k]) custom[k] = this.themes[k];
+ });
+ localStorage.setItem(this.key, JSON.stringify(custom));
+ }
+ registerCommands() {
+ this.commands['theme'] = (args) => this.handle(args);
+ }
+ handle(args) {
+ if (!args[0]) return this.terminal.print('Usage: theme set <name>, theme list, theme new, theme delete <name>, theme export <name>, theme preview <name>, theme keep', 'warning');
+ const sub = args[0].toLowerCase();
+ if (sub === 'set') this.setTheme(args[1]);
+ else if (sub === 'list') this.listThemes();
+ else if (sub === 'new') this.newTheme(args.slice(1));
+ else if (sub === 'delete') this.deleteTheme(args[1]);
+ else if (sub === 'export') this.exportTheme(args[1]);
+ else if (sub === 'preview') this.previewTheme(args[1]);
+ else if (sub === 'keep') this.keepPreview();
+ else this.terminal.print(`Unknown: ${sub}`, 'error');
+ }
+ _applyVars(name) {
+ if (!this.themes[name]) return false;
+ const vars = this.themes[name];
+ const root = document.documentElement;
+ Object.entries(vars).forEach(([key, val]) => root.style.setProperty(key, val));
+ return true;
+ }
+ setTheme(name) {
+ if (!name) return this.terminal.print('Usage: theme set <name>', 'warning');
+ if (!this.themes[name]) return this.terminal.print(`Theme '${name}' not found`, 'error');
+ // Cancel any active preview
+ if (this._previewTimer) {
+ clearTimeout(this._previewTimer);
+ this._previewTimer = null;
+ }
+ this._applyVars(name);
+ localStorage.setItem('webx11_active_theme', name);
+ this.terminal.print(`Theme set to: ${name}`, 'success');
+ }
+ previewTheme(name) {
+ if (!name) return this.terminal.print('Usage: theme preview <name>', 'warning');
+ if (!this.themes[name]) return this.terminal.print(`Theme '${name}' not found`, 'error');
+ // Cancel any existing preview
+ if (this._previewTimer) {
+ clearTimeout(this._previewTimer);
+ this._previewTimer = null;
+ }
+ const previous = localStorage.getItem('webx11_active_theme') || 'dark';
+ this._applyVars(name);
+ this.terminal.print(`🎨 Previewing: ${name} — will revert in 5s`, 'info');
+ this.terminal.print(' Type "theme keep" to keep this theme permanently', 'info');
+ this._previewTimer = setTimeout(() => {
+ this._previewTimer = null;
+ this._applyVars(previous);
+ this.terminal.print(`⏱ Preview ended — reverted to: ${previous}`, 'info');
+ }, 5000);
+ }
+ keepPreview() {
+ if (!this._previewTimer) return this.terminal.print('No active preview to keep', 'warning');
+ clearTimeout(this._previewTimer);
+ this._previewTimer = null;
+ const root = document.documentElement;
+ const cssVars = ['--bg-color', '--bg-secondary', '--bg-tertiary', '--text-color', '--text-muted', '--accent-color', '--border-color'];
+ const currentVars = {};
+ cssVars.forEach(v => { currentVars[v] = getComputedStyle(root).getPropertyValue(v).trim() || '#000'; });
+ let matchName = null;
+ for (const [name, vars] of Object.entries(this.themes)) {
+ const match = cssVars.every(v => vars[v] === currentVars[v]);
+ if (match) { matchName = name; break; }
+ }
+ if (matchName) {
+ localStorage.setItem('webx11_active_theme', matchName);
+ this.terminal.print(`✅ Preview kept — theme saved as: ${matchName}`, 'success');
+ } else {
+ // Custom vars — save current state as active
+ const active = localStorage.getItem('webx11_active_theme') || 'dark';
+ this.terminal.print(`✅ Preview kept (${active} theme)`, 'success');
+ }
+ }
+ listThemes() {
+ const active = localStorage.getItem('webx11_active_theme') || 'dark';
+ this.terminal.print('=== Themes ===', 'info');
+ Object.keys(this.themes).forEach(name => {
+ const mark = name === active ? ' ✅' : '';
+ this.terminal.print(` ${name}${mark}`);
+ });
+ this.terminal.print('Use: theme set <name> to apply', 'info');
+ }
+ newTheme(args) {
+ if (args.length < 1) return this.terminal.print('Usage: theme new <name> --bg #000 --text #0f0', 'warning');
+ const name = args[0].toLowerCase();
+ if (this.themes[name]) return this.terminal.print(`Theme '${name}' already exists`, 'warning');
+ const vars = {};
+ const root = document.documentElement;
+ const cssVars = ['--bg-color', '--bg-secondary', '--bg-tertiary', '--text-color', '--text-muted', '--accent-color', '--border-color', '--error-color', '--warning-color', '--info-color', '--success-color'];
+ for (let i = 1; i < args.length; i++) {
+ if (args[i].startsWith('--')) {
+ const val = args[i + 1];
+ if (val && !val.startsWith('--')) {
+ vars[args[i]] = val;
+ i++;
+ }
+ }
+ }
+ if (Object.keys(vars).length === 0) {
+ const preview = {};
+ cssVars.forEach(v => { preview[v] = getComputedStyle(root).getPropertyValue(v).trim() || '#000'; });
+ this.themes[name] = preview;
+ } else {
+ this.themes[name] = vars;
+ }
+ this.save();
+ this.terminal.print(`Theme '${name}' created with ${Object.keys(this.themes[name]).length} variables`, 'success');
+ }
+ deleteTheme(name) {
+ if (!name) return this.terminal.print('Usage: theme delete <name>', 'warning');
+ if (this.defaults[name]) return this.terminal.print('Cannot delete built-in theme', 'warning');
+ if (!this.themes[name]) return this.terminal.print(`Theme '${name}' not found`, 'error');
+ delete this.themes[name];
+ this.save();
+ const active = localStorage.getItem('webx11_active_theme');
+ if (active === name) { localStorage.removeItem('webx11_active_theme'); }
+ this.terminal.print(`Theme '${name}' deleted`, 'success');
+ }
+ exportTheme(name) {
+ if (!name) return this.terminal.print('Usage: theme export <name>', 'warning');
+ if (!this.themes[name]) return this.terminal.print(`Theme '${name}' not found`, 'error');
+ const data = JSON.stringify(this.themes[name], null, 2);
+ const blob = new Blob([data], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url; a.download = `theme-${name}.json`; a.click();
+ URL.revokeObjectURL(url);
+ this.terminal.print(`Theme '${name}' exported`, 'success');
+ }
+}
+if (typeof window.webx11 !== 'undefined' && window.webx11) {
+ webx11.moduleManager.registerModule(new ThemeModule(), 'https://webx11.duckdns.org/spaces/chatstudio/newos4/modules/theme.js');
+}