Spaces:
Running
Running
| /** | |
| * MorphGuard Context-Aware Theme Integration | |
| * | |
| * This module integrates the dynamic theming system with the application context, | |
| * adapting themes based on: | |
| * 1. User activity and behavior patterns | |
| * 2. Application state and content | |
| * 3. Environmental signals | |
| * 4. Task-specific optimizations | |
| */ | |
| class ContextTheme { | |
| constructor(options = {}) { | |
| // Default options | |
| this.options = { | |
| detectionResultTheming: true, // Change theme based on detection results | |
| dataVisualizationTheming: true, // Optimize for data visualization | |
| attentionModeEnabled: true, // Focus mode for important tasks | |
| securityAlertTheming: true, // Special theme for security alerts | |
| adaptToContentDensity: true, // Adjust based on content density | |
| accessibilityAdaptation: true, // Adjust based on accessibility needs | |
| behavioralAdaptation: true, // Learn from user behavior | |
| ...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; | |
| } | |
| // Application context state | |
| this.appContext = { | |
| currentView: null, | |
| currentTask: null, | |
| detectionResult: null, | |
| securityAlerts: [], | |
| contentDensity: 'normal', | |
| userBehavior: { | |
| prefersDarkMode: null, | |
| prefersHighContrast: null, | |
| clickPatterns: [], | |
| sessionDuration: 0, | |
| frequentActions: {} | |
| }, | |
| accessibilityNeeds: { | |
| reducedMotion: false, | |
| highContrast: false, | |
| largerText: false | |
| }, | |
| environment: { | |
| cpuUsage: null, | |
| memoryUsage: null, | |
| batteryStatus: null, | |
| networkStatus: 'online' | |
| } | |
| }; | |
| // Task-specific theme overrides | |
| this.taskThemes = { | |
| detection: { | |
| variables: { | |
| '--accent-primary': '#3b82f6', // Blue focus for detection | |
| '--card-bg': 'rgba(59, 130, 246, 0.05)' | |
| } | |
| }, | |
| demorph: { | |
| variables: { | |
| '--accent-primary': '#8b5cf6', // Purple focus for demorphing | |
| '--card-bg': 'rgba(139, 92, 246, 0.05)' | |
| } | |
| }, | |
| training: { | |
| variables: { | |
| '--accent-primary': '#10b981', // Green focus for training | |
| '--card-bg': 'rgba(16, 185, 129, 0.05)' | |
| } | |
| }, | |
| security: { | |
| variables: { | |
| '--accent-primary': '#ef4444', // Red focus for security | |
| '--card-bg': 'rgba(239, 68, 68, 0.05)', | |
| '--border-color': 'rgba(239, 68, 68, 0.3)' | |
| } | |
| }, | |
| analytics: { | |
| variables: { | |
| '--accent-primary': '#f59e0b', // Amber focus for analytics | |
| '--card-bg': 'rgba(245, 158, 11, 0.05)', | |
| '--chart-grid': 'rgba(245, 158, 11, 0.2)' | |
| } | |
| } | |
| }; | |
| // Detection result theme modifiers | |
| this.detectionThemes = { | |
| morphed: { | |
| variables: { | |
| '--card-bg': 'var(--detection-morph)', | |
| '--border-color': 'var(--error)' | |
| } | |
| }, | |
| real: { | |
| variables: { | |
| '--card-bg': 'var(--detection-safe)', | |
| '--border-color': 'var(--success)' | |
| } | |
| } | |
| }; | |
| // Initialize | |
| this.init(); | |
| } | |
| /** | |
| * Initialize the context-aware theme integration | |
| */ | |
| init() { | |
| // Detect initial context | |
| this.detectInitialContext(); | |
| // Add observers for context changes | |
| this.setupContextObservers(); | |
| // Add theme manager observer | |
| this.themeManager.addObserver((event) => { | |
| // When theme changes from outside, update our state but don't override | |
| if (event.type === 'themeChange' && !this.isContextOverride) { | |
| this.baseTheme = event.theme; | |
| } | |
| }); | |
| // Store base theme | |
| this.baseTheme = this.themeManager.activeTheme; | |
| // Context override flag | |
| this.isContextOverride = false; | |
| // Expose on window for debugging | |
| window.morphGuardContextTheme = this; | |
| console.log('MorphGuard Context Theme initialized'); | |
| } | |
| /** | |
| * Detect initial application context | |
| */ | |
| detectInitialContext() { | |
| // Check URL for current view | |
| const path = window.location.pathname; | |
| if (path.includes('/demo')) { | |
| this.appContext.currentView = 'demo'; | |
| } else if (path.includes('/setup')) { | |
| this.appContext.currentView = 'setup'; | |
| } else if (path.includes('/lead')) { | |
| this.appContext.currentView = 'lead'; | |
| } else if (path === '/') { | |
| this.appContext.currentView = 'landing'; | |
| } | |
| // Check for reduced motion preference | |
| this.appContext.accessibilityNeeds.reducedMotion = | |
| window.matchMedia('(prefers-reduced-motion: reduce)').matches; | |
| // Update theme based on initial context | |
| this.updateThemeForContext(); | |
| // Detect environment | |
| this.detectEnvironment(); | |
| } | |
| /** | |
| * Setup observers for context changes | |
| */ | |
| setupContextObservers() { | |
| // Watch for view changes via navigation | |
| this.observeNavigation(); | |
| // Watch for detection results | |
| this.observeDetectionResults(); | |
| // Watch for security alerts | |
| this.observeSecurityAlerts(); | |
| // Watch for task changes | |
| this.observeTaskChanges(); | |
| // Track user behavior | |
| this.trackUserBehavior(); | |
| // Monitor environment | |
| this.monitorEnvironment(); | |
| } | |
| /** | |
| * Observe navigation for view changes | |
| */ | |
| observeNavigation() { | |
| // Listen for clicks on navigation links | |
| document.addEventListener('click', (e) => { | |
| try { | |
| // Check if a navigation link was clicked | |
| const link = e.target.closest('a'); | |
| if (link && link.href && link.href.startsWith(window.location.origin)) { | |
| const path = new URL(link.href).pathname; | |
| // Determine view from path | |
| let view = 'unknown'; | |
| if (path.includes('/demo')) { | |
| view = 'demo'; | |
| } else if (path.includes('/setup')) { | |
| view = 'setup'; | |
| } else if (path.includes('/lead')) { | |
| view = 'lead'; | |
| } else if (path === '/') { | |
| view = 'landing'; | |
| } | |
| // Update context if view changed | |
| if (view !== this.appContext.currentView) { | |
| this.appContext.currentView = view; | |
| // Defer theme update to prevent browser from cancelling navigation inside iframes | |
| setTimeout(() => { | |
| this.updateThemeForContext(); | |
| }, 0); | |
| } | |
| } | |
| } catch (err) { | |
| console.error('Error in observeNavigation click listener:', err); | |
| } | |
| }); | |
| // Add history state change listener | |
| window.addEventListener('popstate', () => { | |
| const path = window.location.pathname; | |
| // Determine view from path | |
| let view = 'unknown'; | |
| if (path.includes('/demo')) { | |
| view = 'demo'; | |
| } else if (path.includes('/setup')) { | |
| view = 'setup'; | |
| } else if (path.includes('/lead')) { | |
| view = 'lead'; | |
| } else if (path === '/') { | |
| view = 'landing'; | |
| } | |
| // Update context if view changed | |
| if (view !== this.appContext.currentView) { | |
| this.appContext.currentView = view; | |
| this.updateThemeForContext(); | |
| } | |
| }); | |
| } | |
| /** | |
| * Observe detection results | |
| */ | |
| observeDetectionResults() { | |
| // Watch for detection result elements being added to the DOM | |
| const observer = new MutationObserver((mutations) => { | |
| for (const mutation of mutations) { | |
| if (mutation.type === 'childList') { | |
| // Check for detection result elements | |
| const detectionResult = document.querySelector('.detection-result'); | |
| if (detectionResult) { | |
| // Extract result from data attributes | |
| const isMorphed = detectionResult.getAttribute('data-is-morphed') === 'true'; | |
| const confidence = parseFloat(detectionResult.getAttribute('data-confidence') || '0'); | |
| // Update context | |
| this.appContext.detectionResult = { isMorphed, confidence }; | |
| // Apply theme if enabled | |
| if (this.options.detectionResultTheming) { | |
| this.applyDetectionResultTheme(isMorphed); | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| // Start observing | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| // Listen for custom detection events | |
| document.addEventListener('morphguard:detection', (e) => { | |
| if (e.detail && typeof e.detail.isMorphed !== 'undefined') { | |
| // Update context | |
| this.appContext.detectionResult = { | |
| isMorphed: e.detail.isMorphed, | |
| confidence: e.detail.confidence || 0 | |
| }; | |
| // Apply theme if enabled | |
| if (this.options.detectionResultTheming) { | |
| this.applyDetectionResultTheme(e.detail.isMorphed); | |
| } | |
| } | |
| }); | |
| } | |
| /** | |
| * Observe security alerts | |
| */ | |
| observeSecurityAlerts() { | |
| // Watch for alert elements being added to the DOM | |
| const observer = new MutationObserver((mutations) => { | |
| for (const mutation of mutations) { | |
| if (mutation.type === 'childList') { | |
| // Check for alert elements | |
| const alerts = document.querySelectorAll('.alert-security, .security-alert'); | |
| if (alerts.length > 0) { | |
| // Update context | |
| this.appContext.securityAlerts = Array.from(alerts).map(alert => ({ | |
| message: alert.textContent, | |
| level: alert.getAttribute('data-level') || 'warning', | |
| timestamp: new Date().toISOString() | |
| })); | |
| // Apply theme if enabled | |
| if (this.options.securityAlertTheming && this.appContext.securityAlerts.length > 0) { | |
| this.applySecurityAlertTheme(); | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| // Start observing | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| // Listen for custom alert events | |
| document.addEventListener('morphguard:alert', (e) => { | |
| if (e.detail) { | |
| // Add alert to context | |
| this.appContext.securityAlerts.push({ | |
| message: e.detail.message || 'Security alert', | |
| level: e.detail.level || 'warning', | |
| timestamp: new Date().toISOString() | |
| }); | |
| // Apply theme if enabled | |
| if (this.options.securityAlertTheming && this.appContext.securityAlerts.length > 0) { | |
| this.applySecurityAlertTheme(); | |
| } | |
| } | |
| }); | |
| } | |
| /** | |
| * Observe task changes | |
| */ | |
| observeTaskChanges() { | |
| // Watch for task indicator elements | |
| const observer = new MutationObserver((mutations) => { | |
| for (const mutation of mutations) { | |
| if (mutation.type === 'childList') { | |
| // Check for task indicator elements | |
| const taskIndicator = document.querySelector('[data-current-task]'); | |
| if (taskIndicator) { | |
| const task = taskIndicator.getAttribute('data-current-task'); | |
| if (task && task !== this.appContext.currentTask) { | |
| this.appContext.currentTask = task; | |
| this.applyTaskTheme(task); | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| // Start observing | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| // Check for task buttons | |
| document.addEventListener('click', (e) => { | |
| try { | |
| // Check for task buttons | |
| const taskButton = e.target.closest('[data-task]'); | |
| if (taskButton) { | |
| const task = taskButton.getAttribute('data-task'); | |
| if (task && task !== this.appContext.currentTask) { | |
| this.appContext.currentTask = task; | |
| this.applyTaskTheme(task); | |
| } | |
| } | |
| } catch (err) { | |
| console.error('Error in task button click listener:', err); | |
| } | |
| }); | |
| // Listen for custom task events | |
| document.addEventListener('morphguard:task', (e) => { | |
| if (e.detail && e.detail.task) { | |
| this.appContext.currentTask = e.detail.task; | |
| this.applyTaskTheme(e.detail.task); | |
| } | |
| }); | |
| } | |
| /** | |
| * Track user behavior for behavioral adaptation | |
| */ | |
| trackUserBehavior() { | |
| if (!this.options.behavioralAdaptation) return; | |
| // Session start time | |
| const sessionStart = Date.now(); | |
| // Periodically update session duration | |
| setInterval(() => { | |
| this.appContext.userBehavior.sessionDuration = (Date.now() - sessionStart) / 1000; | |
| }, 60000); // Every minute | |
| document.addEventListener('click', (e) => { | |
| try { | |
| // Record the clicked element | |
| const target = e.target; | |
| if (!target) return; | |
| const elementType = target.tagName ? target.tagName.toLowerCase() : 'unknown'; | |
| const classNames = target.classList ? Array.from(target.classList).join(' ') : ''; | |
| // Add to click patterns | |
| this.appContext.userBehavior.clickPatterns.push({ | |
| elementType, | |
| classNames, | |
| timestamp: new Date().toISOString() | |
| }); | |
| // Limit array size | |
| if (this.appContext.userBehavior.clickPatterns.length > 100) { | |
| this.appContext.userBehavior.clickPatterns.shift(); | |
| } | |
| // Track frequent actions | |
| const actionKey = `${elementType}:${classNames}`; | |
| this.appContext.userBehavior.frequentActions[actionKey] = | |
| (this.appContext.userBehavior.frequentActions[actionKey] || 0) + 1; | |
| // Analyze patterns periodically | |
| this.analyzeUserBehavior(); | |
| } catch (err) { | |
| console.error('Error in click pattern tracking listener:', err); | |
| } | |
| }); | |
| // Check initial theme preference | |
| this.appContext.userBehavior.prefersDarkMode = | |
| this.themeManager.isDarkTheme(this.themeManager.currentTheme); | |
| // Check for high contrast preference | |
| this.appContext.userBehavior.prefersHighContrast = | |
| this.themeManager.currentTheme === 'highContrast'; | |
| // Check for accessibility preferences | |
| const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; | |
| if (reducedMotion) { | |
| this.appContext.accessibilityNeeds.reducedMotion = true; | |
| } | |
| } | |
| /** | |
| * Monitor environment | |
| */ | |
| monitorEnvironment() { | |
| // Check network status | |
| window.addEventListener('online', () => { | |
| this.appContext.environment.networkStatus = 'online'; | |
| }); | |
| window.addEventListener('offline', () => { | |
| this.appContext.environment.networkStatus = 'offline'; | |
| // Apply offline theme if needed | |
| this.applyOfflineTheme(); | |
| }); | |
| // Monitor battery status if available | |
| if ('getBattery' in navigator) { | |
| navigator.getBattery().then(battery => { | |
| // Update battery status | |
| const updateBattery = () => { | |
| this.appContext.environment.batteryStatus = { | |
| level: battery.level, | |
| charging: battery.charging | |
| }; | |
| // Apply low battery theme if needed | |
| if (battery.level < 0.2 && !battery.charging) { | |
| this.applyLowBatteryTheme(); | |
| } | |
| }; | |
| // Listen for battery changes | |
| battery.addEventListener('levelchange', updateBattery); | |
| battery.addEventListener('chargingchange', updateBattery); | |
| // Initial update | |
| updateBattery(); | |
| }); | |
| } | |
| // Monitor content density | |
| this.monitorContentDensity(); | |
| } | |
| /** | |
| * Monitor content density | |
| */ | |
| monitorContentDensity() { | |
| if (!this.options.adaptToContentDensity) return; | |
| // Calculate content density periodically | |
| const calculateDensity = () => { | |
| const contentArea = document.querySelector('main') || document.body; | |
| const contentHeight = contentArea.scrollHeight; | |
| const viewportHeight = window.innerHeight; | |
| const density = contentHeight / viewportHeight; | |
| // Categorize density | |
| let densityCategory = 'normal'; | |
| if (density > 3) { | |
| densityCategory = 'very-high'; | |
| } else if (density > 2) { | |
| densityCategory = 'high'; | |
| } else if (density < 0.5) { | |
| densityCategory = 'low'; | |
| } | |
| // Update context if changed | |
| if (densityCategory !== this.appContext.contentDensity) { | |
| this.appContext.contentDensity = densityCategory; | |
| // Apply theme adjustments if needed | |
| this.applyContentDensityTheme(densityCategory); | |
| } | |
| }; | |
| // Calculate initially and on resize | |
| calculateDensity(); | |
| window.addEventListener('resize', calculateDensity); | |
| // Also recalculate periodically in case content changes | |
| setInterval(calculateDensity, 5000); | |
| } | |
| /** | |
| * Detect environment factors | |
| */ | |
| detectEnvironment() { | |
| // Check network status | |
| this.appContext.environment.networkStatus = navigator.onLine ? 'online' : 'offline'; | |
| // Check if running in a resource-constrained environment | |
| const isLowPower = window.navigator.hardwareConcurrency <= 2; | |
| if (isLowPower) { | |
| // Apply low-power optimizations | |
| this.applyLowPowerTheme(); | |
| } | |
| } | |
| /** | |
| * Update theme based on current context | |
| */ | |
| updateThemeForContext() { | |
| // View-specific theming | |
| switch (this.appContext.currentView) { | |
| case 'demo': | |
| // For demo view, use accent colors that highlight detection functions | |
| this.applyViewTheme('demo'); | |
| break; | |
| case 'setup': | |
| // For setup view, use accent colors that indicate configuration | |
| this.applyViewTheme('setup'); | |
| break; | |
| case 'lead': | |
| // For lead view, use accent colors that are inviting | |
| this.applyViewTheme('lead'); | |
| break; | |
| case 'landing': | |
| // For landing view, use bold, attention-grabbing colors | |
| this.applyViewTheme('landing'); | |
| break; | |
| default: | |
| // Reset to base theme | |
| this.resetToBaseTheme(); | |
| break; | |
| } | |
| } | |
| /** | |
| * Apply theme for specific view | |
| */ | |
| applyViewTheme(view) { | |
| let themeVariables = {}; | |
| switch (view) { | |
| case 'demo': | |
| themeVariables = { | |
| '--accent-primary': '#3b82f6', // Blue | |
| '--accent-secondary': '#60a5fa' | |
| }; | |
| break; | |
| case 'setup': | |
| themeVariables = { | |
| '--accent-primary': '#8b5cf6', // Purple | |
| '--accent-secondary': '#a78bfa' | |
| }; | |
| break; | |
| case 'lead': | |
| themeVariables = { | |
| '--accent-primary': '#10b981', // Green | |
| '--accent-secondary': '#34d399' | |
| }; | |
| break; | |
| case 'landing': | |
| themeVariables = { | |
| '--accent-primary': '#f59e0b', // Amber | |
| '--accent-secondary': '#fbbf24' | |
| }; | |
| break; | |
| } | |
| // Apply temporary theme variables | |
| this.applyThemeVariables(themeVariables); | |
| } | |
| /** | |
| * Apply theme for detection result | |
| */ | |
| applyDetectionResultTheme(isMorphed) { | |
| // Apply theme based on detection result | |
| const themeVariables = isMorphed ? | |
| this.detectionThemes.morphed.variables : | |
| this.detectionThemes.real.variables; | |
| // Apply temporary theme variables | |
| this.applyThemeVariables(themeVariables); | |
| // Set timeout to reset theme after a few seconds | |
| setTimeout(() => { | |
| this.resetToBaseTheme(); | |
| }, 5000); | |
| } | |
| /** | |
| * Apply theme for security alerts | |
| */ | |
| applySecurityAlertTheme() { | |
| // Get the highest severity alert | |
| const alerts = this.appContext.securityAlerts; | |
| let highestSeverity = 'info'; | |
| for (const alert of alerts) { | |
| if (alert.level === 'critical') { | |
| highestSeverity = 'critical'; | |
| break; | |
| } else if (alert.level === 'high' && highestSeverity !== 'critical') { | |
| highestSeverity = 'high'; | |
| } else if (alert.level === 'medium' && highestSeverity !== 'critical' && highestSeverity !== 'high') { | |
| highestSeverity = 'medium'; | |
| } | |
| } | |
| // Apply theme based on severity | |
| let themeVariables = {}; | |
| switch (highestSeverity) { | |
| case 'critical': | |
| themeVariables = { | |
| '--accent-primary': '#dc2626', // Bright red | |
| '--accent-secondary': '#ef4444', | |
| '--border-color': 'rgba(220, 38, 38, 0.5)', | |
| '--card-bg': 'rgba(220, 38, 38, 0.05)' | |
| }; | |
| break; | |
| case 'high': | |
| themeVariables = { | |
| '--accent-primary': '#ea580c', // Orange | |
| '--accent-secondary': '#f97316', | |
| '--border-color': 'rgba(234, 88, 12, 0.5)', | |
| '--card-bg': 'rgba(234, 88, 12, 0.05)' | |
| }; | |
| break; | |
| case 'medium': | |
| themeVariables = { | |
| '--accent-primary': '#f59e0b', // Amber | |
| '--accent-secondary': '#fbbf24', | |
| '--border-color': 'rgba(245, 158, 11, 0.5)', | |
| '--card-bg': 'rgba(245, 158, 11, 0.05)' | |
| }; | |
| break; | |
| default: | |
| themeVariables = { | |
| '--accent-primary': '#3b82f6', // Blue | |
| '--accent-secondary': '#60a5fa', | |
| '--border-color': 'rgba(59, 130, 246, 0.5)', | |
| '--card-bg': 'rgba(59, 130, 246, 0.05)' | |
| }; | |
| } | |
| // Apply temporary theme variables | |
| this.applyThemeVariables(themeVariables); | |
| } | |
| /** | |
| * Apply theme for specific task | |
| */ | |
| applyTaskTheme(task) { | |
| if (!this.taskThemes[task]) { | |
| this.resetToBaseTheme(); | |
| return; | |
| } | |
| // Apply task-specific theme | |
| this.applyThemeVariables(this.taskThemes[task].variables); | |
| } | |
| /** | |
| * Apply theme for offline mode | |
| */ | |
| applyOfflineTheme() { | |
| // Use grayscale theme for offline mode | |
| const themeVariables = { | |
| '--accent-primary': '#6b7280', // Gray | |
| '--accent-secondary': '#9ca3af', | |
| '--border-color': '#d1d5db' | |
| }; | |
| // Apply temporary theme variables | |
| this.applyThemeVariables(themeVariables); | |
| } | |
| /** | |
| * Apply theme for low battery | |
| */ | |
| applyLowBatteryTheme() { | |
| // Force dark theme to save power | |
| if (!this.themeManager.isDarkTheme(this.themeManager.activeTheme)) { | |
| this.isContextOverride = true; | |
| this.themeManager.setTheme('dark'); | |
| this.isContextOverride = false; | |
| } | |
| } | |
| /** | |
| * Apply theme for low-power devices | |
| */ | |
| applyLowPowerTheme() { | |
| // Disable animations | |
| document.documentElement.style.setProperty('--transition-normal', '0s'); | |
| document.documentElement.style.setProperty('--transition-fast', '0s'); | |
| document.documentElement.style.setProperty('--transition-slow', '0s'); | |
| // Use simpler theme | |
| const themeVariables = { | |
| '--shadow-sm': 'none', | |
| '--shadow-md': 'none', | |
| '--shadow-lg': 'none', | |
| '--shadow-xl': 'none' | |
| }; | |
| // Apply theme variables | |
| this.applyThemeVariables(themeVariables); | |
| } | |
| /** | |
| * Apply theme based on content density | |
| */ | |
| applyContentDensityTheme(density) { | |
| let themeVariables = {}; | |
| switch (density) { | |
| case 'very-high': | |
| // For very high density, reduce spacing and increase contrast | |
| themeVariables = { | |
| '--spacing-md': '0.5rem', | |
| '--spacing-lg': '1rem', | |
| '--spacing-xl': '1.5rem', | |
| '--text-primary': this.themeManager.isDarkTheme(this.themeManager.activeTheme) ? | |
| '#ffffff' : '#000000' | |
| }; | |
| break; | |
| case 'high': | |
| // For high density, slightly reduce spacing | |
| themeVariables = { | |
| '--spacing-md': '0.75rem', | |
| '--spacing-lg': '1.25rem', | |
| '--spacing-xl': '1.75rem' | |
| }; | |
| break; | |
| case 'low': | |
| // For low density, increase spacing | |
| themeVariables = { | |
| '--spacing-md': '1.25rem', | |
| '--spacing-lg': '2rem', | |
| '--spacing-xl': '3rem' | |
| }; | |
| break; | |
| default: | |
| // Reset to defaults | |
| themeVariables = { | |
| '--spacing-md': '1rem', | |
| '--spacing-lg': '1.5rem', | |
| '--spacing-xl': '2rem' | |
| }; | |
| } | |
| // Apply theme variables | |
| this.applyThemeVariables(themeVariables); | |
| } | |
| /** | |
| * Apply theme variables without changing base theme | |
| */ | |
| applyThemeVariables(variables) { | |
| // Mark as context override | |
| this.isContextOverride = true; | |
| // Apply variables | |
| for (const [property, value] of Object.entries(variables)) { | |
| document.documentElement.style.setProperty(property, value); | |
| } | |
| // Clear context override flag | |
| this.isContextOverride = false; | |
| } | |
| /** | |
| * Reset to base theme | |
| */ | |
| resetToBaseTheme() { | |
| // Only reset if this is a context override | |
| if (this.isContextOverride) { | |
| // Get current theme configuration | |
| const theme = this.themeManager.themes[this.baseTheme] || this.themeManager.themes.light; | |
| // Reset all variables | |
| for (const [property, value] of Object.entries(theme.variables)) { | |
| document.documentElement.style.setProperty(property, value); | |
| } | |
| // Clear context override flag | |
| this.isContextOverride = false; | |
| } | |
| } | |
| /** | |
| * Analyze user behavior for behavioral adaptation | |
| */ | |
| analyzeUserBehavior() { | |
| // Skip if behavioral adaptation is disabled | |
| if (!this.options.behavioralAdaptation) return; | |
| // Only analyze periodically | |
| if (!this._lastAnalysisTime || (Date.now() - this._lastAnalysisTime > 60000)) { | |
| this._lastAnalysisTime = Date.now(); | |
| // Analyze click patterns | |
| this.analyzeClickPatterns(); | |
| // Check for frequent actions | |
| this.checkFrequentActions(); | |
| // Check for accessibility needs | |
| this.checkAccessibilityNeeds(); | |
| } | |
| } | |
| /** | |
| * Analyze click patterns | |
| */ | |
| analyzeClickPatterns() { | |
| const patterns = this.appContext.userBehavior.clickPatterns; | |
| if (patterns.length < 10) return; | |
| // Example analysis: check if user frequently clicks on the same elements | |
| const elementCounts = {}; | |
| patterns.forEach(pattern => { | |
| const key = `${pattern.elementType}:${pattern.classNames}`; | |
| elementCounts[key] = (elementCounts[key] || 0) + 1; | |
| }); | |
| // Find most frequently clicked elements | |
| const sortedElements = Object.entries(elementCounts) | |
| .sort((a, b) => b[1] - a[1]) | |
| .slice(0, 3); | |
| // If there's a clear favorite, highlight it in the theme | |
| if (sortedElements.length > 0 && sortedElements[0][1] > patterns.length * 0.3) { | |
| const favElement = sortedElements[0][0].split(':')[1]; | |
| // If the favorite element is a navigation item, highlight that section | |
| if (favElement.includes('nav') || favElement.includes('menu')) { | |
| const section = this.getNavigationSection(favElement); | |
| if (section) { | |
| this.highlightSection(section); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Check for frequent actions | |
| */ | |
| checkFrequentActions() { | |
| const actions = this.appContext.userBehavior.frequentActions; | |
| const totalActions = Object.values(actions).reduce((sum, count) => sum + count, 0); | |
| // Skip if not enough actions | |
| if (totalActions < 20) return; | |
| // Check if user frequently performs certain actions | |
| for (const [action, count] of Object.entries(actions)) { | |
| // If action is performed frequently | |
| if (count > totalActions * 0.2) { | |
| // Check for specific actions | |
| if (action.includes('detection') || action.includes('detect')) { | |
| // User frequently uses detection - optimize UI for that | |
| this.optimizeUIForDetection(); | |
| } else if (action.includes('demorph')) { | |
| // User frequently uses demorphing - optimize UI for that | |
| this.optimizeUIForDemorphing(); | |
| } else if (action.includes('training') || action.includes('train')) { | |
| // User frequently uses training - optimize UI for that | |
| this.optimizeUIForTraining(); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Check for accessibility needs | |
| */ | |
| checkAccessibilityNeeds() { | |
| if (!this.options.accessibilityAdaptation) return; | |
| // Check if user might need high contrast | |
| const highContrastClicks = this.appContext.userBehavior.clickPatterns.filter( | |
| pattern => pattern.classNames.includes('high-contrast') || | |
| pattern.classNames.includes('zoom') || | |
| pattern.classNames.includes('accessibility') | |
| ).length; | |
| // If user has clicked on accessibility-related elements | |
| if (highContrastClicks > 0 && !this.appContext.accessibilityNeeds.highContrast) { | |
| this.appContext.accessibilityNeeds.highContrast = true; | |
| // Suggest high contrast theme | |
| this.suggestHighContrastTheme(); | |
| } | |
| // Check if user might need larger text | |
| const textSizeClicks = this.appContext.userBehavior.clickPatterns.filter( | |
| pattern => pattern.classNames.includes('text-size') || | |
| pattern.classNames.includes('font-size') || | |
| pattern.classNames.includes('zoom') | |
| ).length; | |
| // If user has clicked on text size elements | |
| if (textSizeClicks > 0 && !this.appContext.accessibilityNeeds.largerText) { | |
| this.appContext.accessibilityNeeds.largerText = true; | |
| // Apply larger text | |
| this.applyLargerText(); | |
| } | |
| } | |
| /** | |
| * Get navigation section from element class | |
| */ | |
| getNavigationSection(className) { | |
| // Parse class names to find section indicators | |
| const sections = ['detection', 'demorph', 'training', 'setup', 'analytics']; | |
| for (const section of sections) { | |
| if (className.includes(section)) { | |
| return section; | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * Highlight a specific section in the theme | |
| */ | |
| highlightSection(section) { | |
| // Apply task-specific theme if available | |
| if (this.taskThemes[section]) { | |
| this.applyThemeVariables(this.taskThemes[section].variables); | |
| } else { | |
| // Generic highlight | |
| this.applyThemeVariables({ | |
| '--accent-primary': '#3b82f6', | |
| '--card-bg': 'rgba(59, 130, 246, 0.05)' | |
| }); | |
| } | |
| } | |
| /** | |
| * Optimize UI for detection tasks | |
| */ | |
| optimizeUIForDetection() { | |
| // Apply detection-optimized theme | |
| this.applyThemeVariables({ | |
| '--accent-primary': '#3b82f6', // Blue focus for detection | |
| '--card-bg': 'rgba(59, 130, 246, 0.05)' | |
| }); | |
| // Find and enhance detection UI elements | |
| const detectionButtons = document.querySelectorAll('[data-task="detection"], .detection-button'); | |
| detectionButtons.forEach(button => { | |
| button.style.transform = 'scale(1.05)'; | |
| button.style.boxShadow = '0 4px 6px rgba(59, 130, 246, 0.25)'; | |
| }); | |
| } | |
| /** | |
| * Optimize UI for demorphing tasks | |
| */ | |
| optimizeUIForDemorphing() { | |
| // Apply demorphing-optimized theme | |
| this.applyThemeVariables({ | |
| '--accent-primary': '#8b5cf6', // Purple focus for demorphing | |
| '--card-bg': 'rgba(139, 92, 246, 0.05)' | |
| }); | |
| // Find and enhance demorphing UI elements | |
| const demorphButtons = document.querySelectorAll('[data-task="demorph"], .demorph-button'); | |
| demorphButtons.forEach(button => { | |
| button.style.transform = 'scale(1.05)'; | |
| button.style.boxShadow = '0 4px 6px rgba(139, 92, 246, 0.25)'; | |
| }); | |
| } | |
| /** | |
| * Optimize UI for training tasks | |
| */ | |
| optimizeUIForTraining() { | |
| // Apply training-optimized theme | |
| this.applyThemeVariables({ | |
| '--accent-primary': '#10b981', // Green focus for training | |
| '--card-bg': 'rgba(16, 185, 129, 0.05)' | |
| }); | |
| // Find and enhance training UI elements | |
| const trainingButtons = document.querySelectorAll('[data-task="training"], .training-button'); | |
| trainingButtons.forEach(button => { | |
| button.style.transform = 'scale(1.05)'; | |
| button.style.boxShadow = '0 4px 6px rgba(16, 185, 129, 0.25)'; | |
| }); | |
| } | |
| /** | |
| * Suggest high contrast theme | |
| */ | |
| suggestHighContrastTheme() { | |
| // Create suggestion element if not exists | |
| if (!document.querySelector('.theme-suggestion')) { | |
| const suggestion = document.createElement('div'); | |
| suggestion.className = 'theme-suggestion'; | |
| suggestion.innerHTML = ` | |
| <div class="theme-suggestion-content"> | |
| <p>Would you prefer a high contrast theme for better visibility?</p> | |
| <div class="theme-suggestion-actions"> | |
| <button class="btn" id="apply-high-contrast">Yes, enable high contrast</button> | |
| <button class="btn btn-secondary" id="dismiss-suggestion">No thanks</button> | |
| </div> | |
| </div> | |
| `; | |
| // Add styles | |
| suggestion.style.position = 'fixed'; | |
| suggestion.style.bottom = '20px'; | |
| suggestion.style.right = '20px'; | |
| suggestion.style.maxWidth = '300px'; | |
| suggestion.style.backgroundColor = 'var(--card-bg)'; | |
| suggestion.style.borderRadius = 'var(--radius-lg)'; | |
| suggestion.style.boxShadow = 'var(--shadow-lg)'; | |
| suggestion.style.padding = 'var(--spacing-md)'; | |
| suggestion.style.zIndex = '1000'; | |
| // Add to document | |
| document.body.appendChild(suggestion); | |
| // Add event listeners | |
| document.getElementById('apply-high-contrast').addEventListener('click', () => { | |
| this.themeManager.setTheme('highContrast'); | |
| suggestion.remove(); | |
| }); | |
| document.getElementById('dismiss-suggestion').addEventListener('click', () => { | |
| suggestion.remove(); | |
| }); | |
| } | |
| } | |
| /** | |
| * Apply larger text | |
| */ | |
| applyLargerText() { | |
| // Increase font size variables | |
| this.applyThemeVariables({ | |
| '--font-size-xs': '0.875rem', // Increase from 0.75rem | |
| '--font-size-sm': '1rem', // Increase from 0.875rem | |
| '--font-size-md': '1.125rem', // Increase from 1rem | |
| '--font-size-lg': '1.25rem', // Increase from 1.125rem | |
| '--font-size-xl': '1.5rem', // Increase from 1.25rem | |
| '--font-size-2xl': '1.875rem' // Increase from 1.5rem | |
| }); | |
| } | |
| /** | |
| * Get current context state | |
| */ | |
| getContext() { | |
| return { | |
| appContext: { ...this.appContext }, | |
| isContextOverride: this.isContextOverride, | |
| baseTheme: this.baseTheme | |
| }; | |
| } | |
| } | |
| // Initialize when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Check if theme manager is loaded | |
| if (window.morphGuardTheme) { | |
| // Initialize context theme | |
| window.morphGuardContextTheme = new ContextTheme(); | |
| } else { | |
| // Wait for theme manager to load | |
| const checkInterval = setInterval(() => { | |
| if (window.morphGuardTheme) { | |
| window.morphGuardContextTheme = new ContextTheme(); | |
| clearInterval(checkInterval); | |
| } | |
| }, 100); | |
| // Stop checking after 5 seconds | |
| setTimeout(() => { | |
| clearInterval(checkInterval); | |
| }, 5000); | |
| } | |
| }); | |
| // Export for module use | |
| if (typeof module !== 'undefined' && module.exports) { | |
| module.exports = ContextTheme; | |
| } |