/** * MorphGuard Theme Picker Component * * A user interface for selecting and customizing themes, * with visualization of theme colors and preview functionality. */ class ThemePicker { constructor(containerSelector, options = {}) { this.container = document.querySelector(containerSelector); if (!this.container) { console.error(`Theme picker container not found: ${containerSelector}`); return; } // Default options this.options = { showColorSwatches: true, showPreview: true, allowCustomThemes: true, enableRealTimePreview: true, ...options }; // Get theme manager instance this.themeManager = window.morphGuardTheme; if (!this.themeManager) { console.error('Theme manager not found. Ensure dynamic_theme.js is loaded first.'); return; } // State this.selectedTheme = this.themeManager.currentTheme; this.customTheme = { ...this.themeManager.themes.light.variables }; this.previousTheme = null; // Initialize component this.init(); } /** * Initialize the theme picker component */ init() { // Create the component markup this.render(); // Attach event listeners this.attachEventListeners(); // Update display based on current theme this.updateDisplay(); } /** * Render the theme picker component */ render() { // Create container structure this.container.innerHTML = `

Theme Settings

${this.options.showPreview ? `

Preview

Sample Card

This is how content will appear with the selected theme.

Success message
Error message
` : ''}
${this.options.showColorSwatches ? `

Theme Colors

${this.getColorSwatches()}
` : ''} ${this.options.allowCustomThemes ? `

Customize Theme

Modify colors to create your own custom theme.

` : ''}

Theme Preferences

`; // Add styles if not already present if (!document.getElementById('theme-picker-styles')) { const style = document.createElement('style'); style.id = 'theme-picker-styles'; style.textContent = this.getStyles(); document.head.appendChild(style); } } /** * Get theme options HTML for the select dropdown */ getThemeOptions() { const themes = this.themeManager.getAvailableThemes(); return themes.map(theme => { return ``; }).join(''); } /** * Get color swatches HTML */ getColorSwatches() { const colorVars = [ { name: 'Primary Background', var: '--bg-primary' }, { name: 'Secondary Background', var: '--bg-secondary' }, { name: 'Primary Text', var: '--text-primary' }, { name: 'Primary Accent', var: '--accent-primary' }, { name: 'Secondary Accent', var: '--accent-secondary' }, { name: 'Success', var: '--success' }, { name: 'Warning', var: '--warning' }, { name: 'Error', var: '--error' }, { name: 'Info', var: '--info' } ]; return colorVars.map(color => { return `
${color.name}
`; }).join(''); } /** * Get CSS color value for a variable */ getColorValue(variable) { const computedStyle = getComputedStyle(document.documentElement); return computedStyle.getPropertyValue(variable).trim() || '#ffffff'; } /** * Get CSS styles for the theme picker */ getStyles() { return ` .theme-picker { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background-color: var(--card-bg); border-radius: var(--radius-lg); box-shadow: var(--shadow-md); padding: var(--spacing-lg); max-width: 800px; margin: 0 auto; } .theme-picker-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-lg); } .theme-picker-header h3 { margin: 0; } .theme-context-info { font-size: var(--font-size-sm); color: var(--text-secondary); } .theme-selection { margin-bottom: var(--spacing-lg); } .theme-selector { width: 100%; padding: var(--spacing-sm); margin-bottom: var(--spacing-md); border: 1px solid var(--border-color); border-radius: var(--radius-md); background-color: var(--bg-secondary); color: var(--text-primary); } .theme-preview-container { margin-top: var(--spacing-md); } .theme-preview { border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: var(--spacing-md); margin-bottom: var(--spacing-lg); } .theme-preview h4 { margin-top: 0; margin-bottom: var(--spacing-md); } .theme-preview-content { display: flex; flex-wrap: wrap; gap: var(--spacing-md); } .preview-card { background-color: var(--card-bg); padding: var(--spacing-md); border-radius: var(--radius-md); box-shadow: var(--shadow-sm); flex: 1; min-width: 200px; } .preview-card h5 { margin-top: 0; color: var(--text-primary); } .preview-card p { color: var(--text-secondary); } .preview-button { background-color: var(--accent-primary); color: white; border: none; padding: var(--spacing-sm) var(--spacing-md); border-radius: var(--radius-md); cursor: pointer; } .preview-form { flex: 1; min-width: 200px; } .preview-input { width: 100%; padding: var(--spacing-sm); margin-bottom: var(--spacing-md); border: 1px solid var(--border-color); border-radius: var(--radius-md); background-color: var(--bg-secondary); color: var(--text-primary); } .preview-alert { padding: var(--spacing-sm); border-radius: var(--radius-md); margin-bottom: var(--spacing-sm); } .preview-alert.success { background-color: var(--detection-safe); color: var(--success); } .preview-alert.error { background-color: var(--detection-morph); color: var(--error); } .theme-colors { margin-bottom: var(--spacing-lg); } .color-swatches { display: flex; flex-wrap: wrap; gap: var(--spacing-md); } .color-swatch-item { display: flex; flex-direction: column; align-items: center; margin-bottom: var(--spacing-sm); } .color-swatch { width: 40px; height: 40px; border-radius: var(--radius-md); border: 1px solid var(--border-color); margin-bottom: var(--spacing-xs); } .color-swatch-label { font-size: var(--font-size-xs); color: var(--text-secondary); text-align: center; } .custom-theme-section { margin-bottom: var(--spacing-lg); padding: var(--spacing-md); border: 1px solid var(--border-color); border-radius: var(--radius-md); } .custom-theme-controls { display: flex; flex-wrap: wrap; gap: var(--spacing-md); margin-bottom: var(--spacing-md); } .color-picker-group { display: flex; flex-direction: column; } .color-picker-group label { margin-bottom: var(--spacing-xs); font-size: var(--font-size-sm); } .color-picker-group input[type="color"] { width: 50px; height: 30px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); background-color: var(--bg-secondary); } .custom-theme-actions { display: flex; gap: var(--spacing-sm); margin-top: var(--spacing-md); } .theme-preferences { padding: var(--spacing-md); border: 1px solid var(--border-color); border-radius: var(--radius-md); } .preference-group { margin-bottom: var(--spacing-sm); } .preference-group label { display: flex; align-items: center; cursor: pointer; } .preference-group input[type="checkbox"] { margin-right: var(--spacing-sm); } @media (max-width: 600px) { .theme-picker { padding: var(--spacing-md); } .custom-theme-controls, .custom-theme-actions { flex-direction: column; } .color-picker-group { width: 100%; } } `; } /** * Attach event listeners */ attachEventListeners() { // Theme selector const themeSelect = this.container.querySelector('#theme-select'); if (themeSelect) { themeSelect.addEventListener('change', (e) => { this.selectedTheme = e.target.value; this.applySelectedTheme(); }); } // Custom theme color pickers const colorInputs = this.container.querySelectorAll('.color-picker-group input[type="color"]'); colorInputs.forEach(input => { input.addEventListener('input', (e) => { if (this.options.enableRealTimePreview) { this.previewCustomColor(e.target); } }); input.addEventListener('change', (e) => { this.previewCustomColor(e.target); }); }); // Custom theme buttons const applyCustomButton = this.container.querySelector('#apply-custom-theme'); if (applyCustomButton) { applyCustomButton.addEventListener('click', () => { this.applyCustomTheme(); }); } const resetCustomButton = this.container.querySelector('#reset-custom-theme'); if (resetCustomButton) { resetCustomButton.addEventListener('click', () => { this.resetCustomTheme(); }); } const saveCustomButton = this.container.querySelector('#save-custom-theme'); if (saveCustomButton) { saveCustomButton.addEventListener('click', () => { this.saveCustomTheme(); }); } // Preferences checkboxes const autoDetectTimeCheckbox = this.container.querySelector('#auto-detect-time'); if (autoDetectTimeCheckbox) { autoDetectTimeCheckbox.addEventListener('change', (e) => { this.themeManager.options.autoDetectTime = e.target.checked; if (e.target.checked) { this.themeManager.setupTimeBasedTheme(); } }); } const transitionsCheckbox = this.container.querySelector('#transitions-enabled'); if (transitionsCheckbox) { transitionsCheckbox.addEventListener('change', (e) => { this.themeManager.options.enableAnimations = e.target.checked; }); } const highContrastCheckbox = this.container.querySelector('#high-contrast-enabled'); if (highContrastCheckbox) { highContrastCheckbox.addEventListener('change', (e) => { this.themeManager.options.enableHighContrast = e.target.checked; this.render(); // Re-render to update available themes }); } // Listen for theme changes from the theme manager this.themeManager.addObserver((event) => { if (event.type === 'themeChange') { this.updateDisplay(); } }); } /** * Apply the selected theme */ applySelectedTheme() { // Save the previous theme before changing this.previousTheme = this.themeManager.activeTheme; // Apply the new theme this.themeManager.setTheme(this.selectedTheme); // Update display this.updateDisplay(); } /** * Preview a custom color change */ previewCustomColor(inputElement) { const id = inputElement.id; const value = inputElement.value; // Store in custom theme data if (id === 'primary-color') { document.documentElement.style.setProperty('--accent-primary', value); this.customTheme['--accent-primary'] = value; } else if (id === 'background-color') { document.documentElement.style.setProperty('--bg-primary', value); this.customTheme['--bg-primary'] = value; } else if (id === 'text-color') { document.documentElement.style.setProperty('--text-primary', value); this.customTheme['--text-primary'] = value; } else if (id === 'success-color') { document.documentElement.style.setProperty('--success', value); this.customTheme['--success'] = value; } else if (id === 'error-color') { document.documentElement.style.setProperty('--error', value); this.customTheme['--error'] = value; } } /** * Apply custom theme */ applyCustomTheme() { // Get values from inputs const primaryColor = this.container.querySelector('#primary-color').value; const backgroundColor = this.container.querySelector('#background-color').value; const textColor = this.container.querySelector('#text-color').value; const successColor = this.container.querySelector('#success-color').value; const errorColor = this.container.querySelector('#error-color').value; // Create derived colors const primaryLighter = this.adjustColor(primaryColor, 30); const primaryDarker = this.adjustColor(primaryColor, -30); const bgSecondary = this.adjustColor(backgroundColor, -5); const bgTertiary = this.adjustColor(backgroundColor, -10); const textSecondary = this.adjustColor(textColor, 40); const textTertiary = this.adjustColor(textColor, 80); const borderColor = this.adjustColor(backgroundColor, -15); // Generate more robust custom theme const customVariables = { '--bg-primary': backgroundColor, '--bg-secondary': bgSecondary, '--bg-tertiary': bgTertiary, '--text-primary': textColor, '--text-secondary': textSecondary, '--text-tertiary': textTertiary, '--accent-primary': primaryColor, '--accent-secondary': primaryLighter, '--accent-tertiary': this.adjustColor(primaryLighter, 30), '--success': successColor, '--error': errorColor, '--border-color': borderColor, '--card-bg': backgroundColor, '--detection-safe': this.adjustColor(successColor, 80), '--detection-morph': this.adjustColor(errorColor, 80) }; // Store the custom theme this.customTheme = customVariables; // Create and apply the custom theme const customThemeId = this.themeManager.createCustomTheme('Custom', customVariables); this.themeManager.setTheme(customThemeId); // Update the selector to show "Custom" const themeSelect = this.container.querySelector('#theme-select'); if (themeSelect) { themeSelect.value = customThemeId; } // Update selected theme this.selectedTheme = customThemeId; } /** * Reset custom theme to base theme */ resetCustomTheme() { // Reset to previous theme if available if (this.previousTheme) { this.themeManager.setTheme(this.previousTheme); this.selectedTheme = this.previousTheme; // Update select dropdown const themeSelect = this.container.querySelector('#theme-select'); if (themeSelect) { themeSelect.value = this.previousTheme; } } else { // Otherwise reset to light theme this.themeManager.setTheme('light'); this.selectedTheme = 'light'; // Update select dropdown const themeSelect = this.container.querySelector('#theme-select'); if (themeSelect) { themeSelect.value = 'light'; } } // Reset custom theme data this.customTheme = { ...this.themeManager.themes.light.variables }; // Update color picker inputs this.updateColorInputs(); } /** * Save custom theme permanently */ saveCustomTheme() { // Create the custom theme this.themeManager.createCustomTheme('Custom', this.customTheme); // Store the theme preference localStorage.setItem(this.themeManager.options.themeStorageKey, 'custom'); // Update UI this.updateDisplay(); // Show confirmation alert('Custom theme saved successfully!'); } /** * Update display based on current theme */ updateDisplay() { // Update theme selector const themeSelect = this.container.querySelector('#theme-select'); if (themeSelect) { themeSelect.value = this.themeManager.currentTheme; } // Update color inputs this.updateColorInputs(); // Update context info this.updateContextInfo(); } /** * Update color picker inputs to match current theme */ updateColorInputs() { const primaryColorInput = this.container.querySelector('#primary-color'); const backgroundColorInput = this.container.querySelector('#background-color'); const textColorInput = this.container.querySelector('#text-color'); const successColorInput = this.container.querySelector('#success-color'); const errorColorInput = this.container.querySelector('#error-color'); if (primaryColorInput) { primaryColorInput.value = this.getColorValue('--accent-primary'); } if (backgroundColorInput) { backgroundColorInput.value = this.getColorValue('--bg-primary'); } if (textColorInput) { textColorInput.value = this.getColorValue('--text-primary'); } if (successColorInput) { successColorInput.value = this.getColorValue('--success'); } if (errorColorInput) { errorColorInput.value = this.getColorValue('--error'); } } /** * Update theme context information */ updateContextInfo() { const contextInfo = this.container.querySelector('.theme-context-info'); if (!contextInfo) return; const themeContext = this.themeManager.getThemeContext(); let contextText = ''; if (themeContext.currentTheme === 'system') { contextText = `Using system preference: ${themeContext.activeTheme}`; } else if (themeContext.context.time && this.themeManager.options.autoDetectTime) { const timeContext = themeContext.context.time; if (timeContext.isNight) { contextText = 'Night mode active'; } else if (timeContext.isEvening) { contextText = 'Evening mode active'; } else if (timeContext.isMorning) { contextText = 'Morning mode active'; } else if (timeContext.isAfternoon) { contextText = 'Day mode active'; } } contextInfo.textContent = contextText; } /** * Adjust a color by a percentage * Positive percentage lightens, negative darkens */ adjustColor(color, percent) { // Convert hex to RGB let r = parseInt(color.substring(1, 3), 16); let g = parseInt(color.substring(3, 5), 16); let b = parseInt(color.substring(5, 7), 16); // Adjust the color if (percent > 0) { // Lighten r = Math.min(255, Math.round(r + (255 - r) * (percent / 100))); g = Math.min(255, Math.round(g + (255 - g) * (percent / 100))); b = Math.min(255, Math.round(b + (255 - b) * (percent / 100))); } else { // Darken const p = Math.abs(percent) / 100; r = Math.max(0, Math.round(r * (1 - p))); g = Math.max(0, Math.round(g * (1 - p))); b = Math.max(0, Math.round(b * (1 - p))); } // Convert back to hex return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; } } // Initialize ThemePicker when DOM is loaded document.addEventListener('DOMContentLoaded', () => { // Check if we should auto-initialize const autoInitPicker = document.querySelector('[data-theme-picker="auto"]'); if (autoInitPicker) { new ThemePicker('[data-theme-picker="auto"]'); } }); // Export for module use if (typeof module !== 'undefined' && module.exports) { module.exports = ThemePicker; }