UNPKG

claritykit-svelte

Version:

A comprehensive Svelte component library focused on accessibility, ADHD-optimized design, developer experience, and full SSR compatibility

564 lines (534 loc) 18.6 kB
/** * Cognitive Load Management for ADHD-Focused Components * * Provides intelligent cognitive load reduction, motion management, and * interface simplification strategies specifically designed for ADHD users. */ // Default configuration const defaultConfig = { respectReducedMotion: true, enableAdaptiveAnimations: true, defaultAnimationDuration: 200, emergencyMotionDisable: false, enableProgressiveDisclosure: true, maxVisibleOptions: 4, autoHideSecondaryActions: true, simplifyOnCognitivePressure: true, enableFocusAssist: true, highlightActiveElements: true, dimInactiveElements: false, enableBreakReminders: true, chunkedInformationDelivery: true, maxInformationUnits: 3, enableContextualGuidance: true, prioritizeEssentialInfo: true, extendedTimeouts: true, adaptivePacing: true, enablePauseAnywhere: true, defaultPaceMultiplier: 1.5 }; let config = { ...defaultConfig }; let currentCognitiveState = { overloadLevel: 'none', attentionSpan: 'medium', processingSpeed: 'normal', lastBreakTime: Date.now(), cognitiveEffort: 0, taskComplexity: 'simple' }; /** * Initialize cognitive load management system */ export function initCognitiveLoadManagement(userConfig) { if (userConfig) { config = { ...defaultConfig, ...userConfig }; } // Detect system motion preferences detectMotionPreferences(); // Set up monitoring setupCognitiveLoadMonitoring(); // Apply initial settings applyCognitiveLoadStyles(); } /** * Detect and respect system motion preferences */ export function detectMotionPreferences() { const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const prefersNoAnimation = window.matchMedia('(prefers-reduced-motion: no-preference)').matches === false; const preferences = { reduceMotion: prefersReducedMotion || config.emergencyMotionDisable, allowEssentialMotion: !prefersNoAnimation && config.enableAdaptiveAnimations, preferStaticFeedback: prefersReducedMotion, customAnimationSpeed: prefersReducedMotion ? 0.1 : 1.0, emergencyStopMotion: config.emergencyMotionDisable }; // Listen for changes in motion preferences window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', (e) => { preferences.reduceMotion = e.matches || config.emergencyMotionDisable; preferences.preferStaticFeedback = e.matches; preferences.customAnimationSpeed = e.matches ? 0.1 : 1.0; applyCognitiveLoadStyles(); }); return preferences; } /** * Apply motion-safe animations for ADHD users */ export function applyMotionSafeAnimation(element, animation) { const motionPrefs = detectMotionPreferences(); // Check if motion should be disabled if (motionPrefs.reduceMotion && !animation.essential) { applyStaticFeedback(element, animation.fallback || 'static-highlight'); return; } // Apply reduced or adapted motion const duration = animation.duration || config.defaultAnimationDuration; const adjustedDuration = motionPrefs.customAnimationSpeed * duration; element.style.setProperty('--motion-duration', `${adjustedDuration}ms`); element.style.setProperty('--motion-easing', 'ease-out'); switch (animation.type) { case 'fade': element.classList.add('ck-motion-fade'); break; case 'slide': element.classList.add('ck-motion-slide'); break; case 'scale': element.classList.add('ck-motion-scale'); break; case 'pulse': if (!motionPrefs.reduceMotion) { element.classList.add('ck-motion-pulse'); } else { applyStaticFeedback(element, 'static-highlight'); } break; case 'essential-only': // Only apply if absolutely necessary for usability if (animation.essential) { element.classList.add('ck-motion-essential'); } break; } // Auto-remove animation classes setTimeout(() => { element.classList.remove('ck-motion-fade', 'ck-motion-slide', 'ck-motion-scale', 'ck-motion-pulse', 'ck-motion-essential'); }, adjustedDuration + 100); } /** * Apply static feedback as alternative to motion */ export function applyStaticFeedback(element, type = 'static-highlight') { switch (type) { case 'static-highlight': element.classList.add('ck-static-highlight'); setTimeout(() => element.classList.remove('ck-static-highlight'), 2000); break; case 'border-emphasis': element.classList.add('ck-border-emphasis'); setTimeout(() => element.classList.remove('ck-border-emphasis'), 3000); break; case 'instant': // Immediate visual change without transition element.classList.add('ck-instant-feedback'); setTimeout(() => element.classList.remove('ck-instant-feedback'), 100); break; case 'none': default: // No visual feedback break; } } /** * Assess and manage cognitive load levels */ export function assessCognitiveLoad(indicators) { let overloadLevel = 'none'; let cognitiveEffort = 0; // Analyze indicators if (indicators.taskSwitches > 5) cognitiveEffort += 20; if (indicators.errorCount > 3) cognitiveEffort += 25; if (indicators.hesitationTime > 5000) cognitiveEffort += 15; if (indicators.focusTime < 1000) cognitiveEffort += 10; if (indicators.interactionSpeed < 0.5) cognitiveEffort += 20; // Determine overload level if (cognitiveEffort >= 70) overloadLevel = 'severe'; else if (cognitiveEffort >= 50) overloadLevel = 'moderate'; else if (cognitiveEffort >= 30) overloadLevel = 'mild'; // Update cognitive state currentCognitiveState = { ...currentCognitiveState, overloadLevel, cognitiveEffort, attentionSpan: indicators.focusTime > 10000 ? 'long' : indicators.focusTime > 3000 ? 'medium' : 'short', processingSpeed: indicators.interactionSpeed > 1.5 ? 'fast' : indicators.interactionSpeed > 0.8 ? 'normal' : 'slow' }; // Apply adaptive strategies if (config.simplifyOnCognitivePressure && overloadLevel !== 'none') { applyLoadReductionStrategies(overloadLevel); } return currentCognitiveState; } /** * Apply cognitive load reduction strategies */ export function applyLoadReductionStrategies(level) { const body = document.body; // Remove existing load classes body.classList.remove('ck-load-mild', 'ck-load-moderate', 'ck-load-severe'); switch (level) { case 'severe': body.classList.add('ck-load-severe'); // Hide non-essential elements document.querySelectorAll('[data-importance="low"]').forEach(el => { el.style.display = 'none'; }); // Increase text size and spacing body.style.setProperty('--ck-text-scale', '1.2'); body.style.setProperty('--ck-space-scale', '1.3'); break; case 'moderate': body.classList.add('ck-load-moderate'); // Dim secondary elements document.querySelectorAll('[data-importance="medium"]').forEach(el => { el.style.opacity = '0.7'; }); body.style.setProperty('--ck-text-scale', '1.1'); break; case 'mild': body.classList.add('ck-load-mild'); // Subtle visual simplification body.style.setProperty('--ck-shadow-scale', '0.8'); body.style.setProperty('--ck-border-scale', '0.9'); break; case 'none': default: // Reset to default body.style.removeProperty('--ck-text-scale'); body.style.removeProperty('--ck-space-scale'); body.style.removeProperty('--ck-shadow-scale'); body.style.removeProperty('--ck-border-scale'); // Restore hidden elements document.querySelectorAll('[data-importance]').forEach(el => { el.style.display = ''; el.style.opacity = ''; }); break; } } /** * Implement progressive disclosure for complex interfaces */ export function implementProgressiveDisclosure(container, options) { const items = Array.from(container.children); const { initialVisible, expandTrigger, expandText, collapseText, animateExpansion } = options; if (items.length <= initialVisible) return; // No need for disclosure // Hide items beyond initial visible count items.slice(initialVisible).forEach(item => { item.style.display = 'none'; item.setAttribute('data-disclosed', 'false'); }); // Create expand/collapse trigger const trigger = document.createElement('button'); trigger.textContent = expandText; trigger.className = 'ck-disclosure-trigger'; trigger.setAttribute('aria-expanded', 'false'); trigger.setAttribute('aria-label', `Show ${items.length - initialVisible} more items`); let expanded = false; trigger.addEventListener('click', () => { expanded = !expanded; items.slice(initialVisible).forEach(item => { if (expanded) { item.style.display = ''; item.setAttribute('data-disclosed', 'true'); if (animateExpansion && !detectMotionPreferences().reduceMotion) { applyMotionSafeAnimation(item, { type: 'fade', duration: 200 }); } } else { item.style.display = 'none'; item.setAttribute('data-disclosed', 'false'); } }); trigger.textContent = expanded ? collapseText : expandText; trigger.setAttribute('aria-expanded', String(expanded)); trigger.setAttribute('aria-label', expanded ? 'Show fewer items' : `Show ${items.length - initialVisible} more items`); }); container.appendChild(trigger); } /** * Create cognitive break reminders */ export function createBreakReminder(config) { if (!config || !config.enableBreakReminders) return; const intervalMs = config.interval * 60 * 1000; setInterval(() => { const timeSinceLastBreak = Date.now() - currentCognitiveState.lastBreakTime; if (timeSinceLastBreak >= intervalMs) { showBreakReminder(config); } }, 60000); // Check every minute } function showBreakReminder(config) { const reminder = document.createElement('div'); reminder.className = 'ck-break-reminder'; reminder.setAttribute('role', 'alert'); reminder.setAttribute('aria-live', 'assertive'); reminder.innerHTML = ` <div class="ck-break-reminder__content"> <span class="ck-break-reminder__icon">⏰</span> <span class="ck-break-reminder__message">${config.message}</span> ${config.dismissible ? '<button class="ck-break-reminder__dismiss" aria-label="Dismiss reminder">×</button>' : ''} </div> `; document.body.appendChild(reminder); // Add dismiss functionality if (config.dismissible) { const dismissBtn = reminder.querySelector('.ck-break-reminder__dismiss'); dismissBtn?.addEventListener('click', () => { reminder.remove(); currentCognitiveState.lastBreakTime = Date.now(); }); } // Auto-hide setTimeout(() => { if (document.body.contains(reminder)) { reminder.remove(); } }, config.autoHide * 1000); } /** * Apply cognitive load management styles */ function applyCognitiveLoadStyles() { if (document.getElementById('ck-cognitive-load-styles')) return; const style = document.createElement('style'); style.id = 'ck-cognitive-load-styles'; style.textContent = getCognitiveLoadCSS(); document.head.appendChild(style); } /** * Set up cognitive load monitoring */ function setupCognitiveLoadMonitoring() { let interactionCount = 0; let errorCount = 0; let lastInteractionTime = Date.now(); // Monitor interactions document.addEventListener('click', () => { interactionCount++; const now = Date.now(); const timeDiff = now - lastInteractionTime; // Assess based on interaction patterns if (timeDiff < 500) { // Rapid clicking assessCognitiveLoad({ taskSwitches: interactionCount, errorCount, hesitationTime: 0, focusTime: timeDiff, interactionSpeed: 1000 / timeDiff }); } lastInteractionTime = now; }); // Monitor errors window.addEventListener('error', () => { errorCount++; }); // Reset counters periodically setInterval(() => { interactionCount = 0; errorCount = 0; }, 60000); } /** * Get current cognitive state */ export function getCognitiveState() { return { ...currentCognitiveState }; } /** * Update cognitive load configuration */ export function updateCognitiveLoadConfig(updates) { config = { ...config, ...updates }; applyCognitiveLoadStyles(); } /** * CSS for cognitive load management */ export function getCognitiveLoadCSS() { return ` /* Motion-safe animations */ .ck-motion-fade { opacity: 0; animation: ck-fade-in var(--motion-duration, 200ms) var(--motion-easing, ease-out) forwards; } .ck-motion-slide { transform: translateY(10px); opacity: 0; animation: ck-slide-in var(--motion-duration, 200ms) var(--motion-easing, ease-out) forwards; } .ck-motion-scale { transform: scale(0.95); opacity: 0; animation: ck-scale-in var(--motion-duration, 200ms) var(--motion-easing, ease-out) forwards; } .ck-motion-pulse { animation: ck-gentle-pulse calc(var(--motion-duration, 200ms) * 3) ease-in-out; } .ck-motion-essential { transition: opacity var(--motion-duration, 200ms) var(--motion-easing, ease-out); } /* Static feedback alternatives */ .ck-static-highlight { background-color: var(--ck-accent-bg, rgba(0, 102, 204, 0.1)) !important; border: 2px solid var(--ck-accent, #0066cc) !important; } .ck-border-emphasis { border-width: 3px !important; border-style: solid !important; border-color: var(--ck-accent, #0066cc) !important; } .ck-instant-feedback { transform: scale(1.02); box-shadow: 0 0 0 4px var(--ck-accent-alpha, rgba(0, 102, 204, 0.3)); } /* Cognitive load states */ .ck-load-mild { --ck-shadow-scale: 0.8; --ck-border-scale: 0.9; } .ck-load-moderate { --ck-text-scale: 1.1; } .ck-load-moderate [data-importance="medium"] { opacity: 0.7; } .ck-load-severe { --ck-text-scale: 1.2; --ck-space-scale: 1.3; } .ck-load-severe [data-importance="low"] { display: none !important; } /* Progressive disclosure */ .ck-disclosure-trigger { margin-top: var(--ck-space-3, 0.75rem); padding: var(--ck-space-2, 0.5rem) var(--ck-space-3, 0.75rem); background: var(--ck-bg-secondary, #f8f9fa); border: 1px solid var(--ck-border, #e5e7eb); border-radius: var(--ck-radius-md, 0.375rem); cursor: pointer; font-size: var(--ck-text-sm, 0.875rem); color: var(--ck-text-secondary, #6b7280); } .ck-disclosure-trigger:hover { background: var(--ck-bg-hover, #f1f5f9); } .ck-disclosure-trigger:focus-visible { outline: 2px solid var(--ck-accent, #0066cc); outline-offset: 2px; } /* Break reminders */ .ck-break-reminder { position: fixed; top: var(--ck-space-4, 1rem); right: var(--ck-space-4, 1rem); background: var(--ck-bg-primary, white); border: 2px solid var(--ck-warning, #f59e0b); border-radius: var(--ck-radius-lg, 0.5rem); padding: var(--ck-space-4, 1rem); box-shadow: var(--ck-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.1)); z-index: 9999; max-width: 320px; } .ck-break-reminder__content { display: flex; align-items: center; gap: var(--ck-space-3, 0.75rem); } .ck-break-reminder__icon { font-size: 1.5rem; flex-shrink: 0; } .ck-break-reminder__message { flex: 1; font-size: var(--ck-text-sm, 0.875rem); color: var(--ck-text-primary, #1f2937); } .ck-break-reminder__dismiss { background: none; border: none; font-size: 1.25rem; cursor: pointer; color: var(--ck-text-muted, #6b7280); padding: var(--ck-space-1, 0.25rem); border-radius: var(--ck-radius-sm, 0.25rem); } .ck-break-reminder__dismiss:hover { background: var(--ck-bg-hover, #f1f5f9); } /* Animations */ @keyframes ck-fade-in { to { opacity: 1; } } @keyframes ck-slide-in { to { transform: translateY(0); opacity: 1; } } @keyframes ck-scale-in { to { transform: scale(1); opacity: 1; } } @keyframes ck-gentle-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.8; } } /* Respect reduced motion preference */ @media (prefers-reduced-motion: reduce) { .ck-motion-fade, .ck-motion-slide, .ck-motion-scale, .ck-motion-pulse, .ck-motion-essential { animation: none !important; transition: none !important; } .ck-instant-feedback { transform: none; } } /* High contrast support */ @media (prefers-contrast: high) { .ck-static-highlight, .ck-border-emphasis { border-color: currentColor !important; background-color: transparent !important; } .ck-break-reminder { border-color: currentColor; border-width: 3px; } } `; }