Files
nostr_login_lite/themes/theme-manager.js
2025-09-14 18:51:27 -04:00

286 lines
7.9 KiB
JavaScript

/**
* 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;
}