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
JavaScript
/**
* 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,
}
.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,
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,
}
.ck-disclosure-trigger:hover {
background: var(--ck-bg-hover,
}
.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,
}
.ck-break-reminder__dismiss {
background: none;
border: none;
font-size: 1.25rem;
cursor: pointer;
color: var(--ck-text-muted,
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,
}
/* 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;
}
}
`;
}