/** * NOSTR_LOGIN_LITE Theme Manager * Handles theme loading, switching, and CSS custom property management */ class NostrThemeManager { constructor() { this.currentTheme = null; this.availableThemes = new Map(); this.themeCache = new Map(); this.basePath = './themes/'; // Initialize with default theme this.init(); } async init() { try { // Load available themes index await this.loadThemeIndex(); // Set default theme if none is set if (!this.currentTheme) { await this.loadTheme('default'); } console.log('NostrThemeManager: Initialized with themes:', Array.from(this.availableThemes.keys())); } catch (error) { console.error('NostrThemeManager: Initialization failed:', error); this.fallbackToInlineStyles(); } } async loadThemeIndex() { // For now, we'll manually register available themes // In production, this could fetch from a themes.json index file this.availableThemes.set('default', { name: 'Default Monospace', path: 'default', description: 'Black/white/red monospace theme' }); this.availableThemes.set('dark', { name: 'Dark Monospace', path: 'dark', description: 'Dark mode with green accents and monospace typography' }); // Future themes can be registered here or loaded from an index // this.availableThemes.set('cyberpunk', { ... }); } async loadTheme(themeName) { try { console.log(`NostrThemeManager: Loading theme "${themeName}"`); // Check if theme exists if (!this.availableThemes.has(themeName)) { throw new Error(`Theme "${themeName}" not found`); } // Check cache first if (this.themeCache.has(themeName)) { const cachedTheme = this.themeCache.get(themeName); this.applyTheme(cachedTheme); this.currentTheme = themeName; return cachedTheme; } // Load theme metadata const themeInfo = this.availableThemes.get(themeName); const metadataUrl = `${this.basePath}${themeInfo.path}/theme.json`; const response = await fetch(metadataUrl); if (!response.ok) { throw new Error(`Failed to load theme metadata: ${response.statusText}`); } const themeData = await response.json(); // Validate theme data this.validateThemeData(themeData); // Load CSS file await this.loadThemeCSS(themeInfo.path); // Cache the theme data this.themeCache.set(themeName, themeData); // Apply the theme this.applyTheme(themeData); this.currentTheme = themeName; console.log(`NostrThemeManager: Successfully loaded theme "${themeName}"`); // Dispatch theme change event this.dispatchThemeChangeEvent(themeName, themeData); return themeData; } catch (error) { console.error(`NostrThemeManager: Failed to load theme "${themeName}":`, error); throw error; } } async loadThemeCSS(themePath) { const cssUrl = `${this.basePath}${themePath}/theme.css`; // Remove existing theme CSS const existingThemeCSS = document.getElementById('nl-theme-css'); if (existingThemeCSS) { existingThemeCSS.remove(); } // Load new theme CSS const link = document.createElement('link'); link.id = 'nl-theme-css'; link.rel = 'stylesheet'; link.type = 'text/css'; link.href = cssUrl; // Wait for CSS to load await new Promise((resolve, reject) => { link.onload = resolve; link.onerror = () => reject(new Error(`Failed to load CSS from ${cssUrl}`)); document.head.appendChild(link); }); } applyTheme(themeData) { if (!themeData.variables) { console.warn('NostrThemeManager: Theme data has no variables to apply'); return; } const root = document.documentElement; // Apply CSS custom properties Object.entries(themeData.variables).forEach(([property, value]) => { root.style.setProperty(property, value); }); console.log(`NostrThemeManager: Applied ${Object.keys(themeData.variables).length} CSS variables`); } validateThemeData(themeData) { const required = ['name', 'version', 'variables']; for (const field of required) { if (!themeData[field]) { throw new Error(`Theme validation failed: missing required field "${field}"`); } } if (typeof themeData.variables !== 'object') { throw new Error('Theme validation failed: variables must be an object'); } } fallbackToInlineStyles() { console.log('NostrThemeManager: Falling back to inline styles'); // Apply default theme variables directly const defaultVariables = { '--nl-primary-color': '#000000', '--nl-secondary-color': '#ffffff', '--nl-accent-color': '#ff0000', '--nl-font-family': '"Courier New", Courier, monospace', '--nl-border-radius': '15px', '--nl-border-width': '3px', '--nl-border-style': 'solid', '--nl-padding-button': '12px 16px', '--nl-padding-container': '20px 24px', '--nl-font-size-base': '14px', '--nl-font-size-title': '24px', '--nl-font-size-button': '16px', '--nl-transition-duration': '0.2s' }; const root = document.documentElement; Object.entries(defaultVariables).forEach(([property, value]) => { root.style.setProperty(property, value); }); this.currentTheme = 'fallback'; } dispatchThemeChangeEvent(themeName, themeData) { if (typeof window !== 'undefined') { const event = new CustomEvent('nlThemeChanged', { detail: { theme: themeName, data: themeData, timestamp: Date.now() } }); window.dispatchEvent(event); } } // Public API methods getCurrentTheme() { return this.currentTheme; } getAvailableThemes() { return Array.from(this.availableThemes.keys()); } getThemeInfo(themeName) { return this.availableThemes.get(themeName); } async switchTheme(themeName) { return await this.loadTheme(themeName); } getThemeVariable(variableName) { if (typeof window === 'undefined') return null; const root = document.documentElement; const style = getComputedStyle(root); return style.getPropertyValue(variableName); } setThemeVariable(variableName, value) { if (typeof window === 'undefined') return; const root = document.documentElement; root.style.setProperty(variableName, value); } resetTheme() { const root = document.documentElement; // Remove all nl- prefixed custom properties const style = getComputedStyle(root); for (let i = 0; i < style.length; i++) { const property = style[i]; if (property.startsWith('--nl-')) { root.style.removeProperty(property); } } // Remove theme CSS const themeCSS = document.getElementById('nl-theme-css'); if (themeCSS) { themeCSS.remove(); } this.currentTheme = null; } // Theme creation utilities (for developers) exportCurrentTheme() { const root = document.documentElement; const style = getComputedStyle(root); const variables = {}; for (let i = 0; i < style.length; i++) { const property = style[i]; if (property.startsWith('--nl-')) { variables[property] = style.getPropertyValue(property); } } return { name: 'Custom Theme', version: '1.0.0', author: 'User', description: 'Exported theme', variables, timestamp: new Date().toISOString() }; } } // Export for use in NOSTR_LOGIN_LITE if (typeof window !== 'undefined') { window.NostrThemeManager = NostrThemeManager; console.log('NostrThemeManager: Class available globally'); } else { // Node.js environment module.exports = NostrThemeManager; }