MorphGuard / static /js /theme_picker.js
juanquy's picture
Initial clean commit of modular MorphGuard
2978bba
Raw
History Blame Contribute Delete
24.8 kB
/**
* 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 = `
<div class="theme-picker">
<div class="theme-picker-header">
<h3>Theme Settings</h3>
<div class="theme-context-info"></div>
</div>
<div class="theme-selection">
<label for="theme-select">Select Theme:</label>
<select id="theme-select" class="theme-selector">
${this.getThemeOptions()}
</select>
<div class="theme-preview-container">
${this.options.showPreview ? `
<div class="theme-preview">
<h4>Preview</h4>
<div class="theme-preview-content">
<div class="preview-card">
<h5>Sample Card</h5>
<p>This is how content will appear with the selected theme.</p>
<button class="preview-button">Button</button>
</div>
<div class="preview-form">
<input type="text" placeholder="Text input" class="preview-input">
<div class="preview-alert success">Success message</div>
<div class="preview-alert error">Error message</div>
</div>
</div>
</div>
` : ''}
</div>
</div>
${this.options.showColorSwatches ? `
<div class="theme-colors">
<h4>Theme Colors</h4>
<div class="color-swatches">
${this.getColorSwatches()}
</div>
</div>
` : ''}
${this.options.allowCustomThemes ? `
<div class="custom-theme-section">
<h4>Customize Theme</h4>
<p>Modify colors to create your own custom theme.</p>
<div class="custom-theme-controls">
<div class="color-picker-group">
<label for="primary-color">Primary Color:</label>
<input type="color" id="primary-color" name="primary-color" value="${this.getColorValue('--accent-primary')}">
</div>
<div class="color-picker-group">
<label for="background-color">Background:</label>
<input type="color" id="background-color" name="background-color" value="${this.getColorValue('--bg-primary')}">
</div>
<div class="color-picker-group">
<label for="text-color">Text Color:</label>
<input type="color" id="text-color" name="text-color" value="${this.getColorValue('--text-primary')}">
</div>
<div class="color-picker-group">
<label for="success-color">Success Color:</label>
<input type="color" id="success-color" name="success-color" value="${this.getColorValue('--success')}">
</div>
<div class="color-picker-group">
<label for="error-color">Error Color:</label>
<input type="color" id="error-color" name="error-color" value="${this.getColorValue('--error')}">
</div>
</div>
<div class="custom-theme-actions">
<button id="apply-custom-theme" class="btn">Apply Custom Theme</button>
<button id="reset-custom-theme" class="btn btn-secondary">Reset</button>
<button id="save-custom-theme" class="btn">Save as Custom Theme</button>
</div>
</div>
` : ''}
<div class="theme-preferences">
<h4>Theme Preferences</h4>
<div class="preference-group">
<label for="auto-detect-time">
<input type="checkbox" id="auto-detect-time" ${this.themeManager.options.autoDetectTime ? 'checked' : ''}>
Automatically switch theme based on time of day
</label>
</div>
<div class="preference-group">
<label for="transitions-enabled">
<input type="checkbox" id="transitions-enabled" ${this.themeManager.options.enableAnimations ? 'checked' : ''}>
Enable smooth theme transitions
</label>
</div>
<div class="preference-group">
<label for="high-contrast-enabled">
<input type="checkbox" id="high-contrast-enabled" ${this.themeManager.options.enableHighContrast ? 'checked' : ''}>
Show high contrast theme option
</label>
</div>
</div>
</div>
`;
// 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 `<option value="${theme.id}" ${theme.id === this.selectedTheme ? 'selected' : ''}>
${theme.name}
</option>`;
}).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 `
<div class="color-swatch-item">
<div class="color-swatch" style="background-color: var(${color.var});" data-color-var="${color.var}"></div>
<div class="color-swatch-label">${color.name}</div>
</div>
`;
}).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;
}