UNPKG

claritykit-svelte

Version:

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

381 lines (380 loc) 11.9 kB
// Collaboration utilities for ClarityKit data visualizations /** * Generate a random user color from a predefined palette */ export function generateUserColor() { const colors = [ '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9', '#F8C471', '#82E0AA', '#AED6F1', '#F7B2D5', '#D7BDE2' ]; return colors[Math.floor(Math.random() * colors.length)]; } /** * Generate a unique room ID for collaboration */ export function generateRoomId(prefix = 'chart') { const timestamp = Date.now().toString(36); const random = Math.random().toString(36).substring(2, 8); return `${prefix}-${timestamp}-${random}`; } /** * Validate and sanitize user input for collaboration */ export function sanitizeUserInput(input) { return input .trim() .replace(/[<>]/g, '') // Remove potential HTML .substring(0, 100); // Limit length } /** * Calculate relative position from absolute coordinates */ export function getRelativePosition(clientX, clientY, container) { const rect = container.getBoundingClientRect(); const x = ((clientX - rect.left) / rect.width) * 100; const y = ((clientY - rect.top) / rect.height) * 100; return { x: Math.max(0, Math.min(100, x)), y: Math.max(0, Math.min(100, y)) }; } /** * Throttle function calls for performance */ export function throttle(func, delay) { let timeoutId = null; let lastExecTime = 0; return (...args) => { const currentTime = Date.now(); if (currentTime - lastExecTime > delay) { func.apply(null, args); lastExecTime = currentTime; } else { if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout(() => { func.apply(null, args); lastExecTime = Date.now(); }, delay - (currentTime - lastExecTime)); } }; } /** * Debounce function calls */ export function debounce(func, delay) { let timeoutId = null; return (...args) => { if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(null, args), delay); }; } /** * Create a conflict-free replicated data type (CRDT) for chart data */ export class ChartDataCRDT { constructor() { Object.defineProperty(this, "data", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "timestamps", { enumerable: true, configurable: true, writable: true, value: new Map() }); } set(key, value, timestamp = Date.now()) { const existingTimestamp = this.timestamps.get(key) || 0; if (timestamp >= existingTimestamp) { this.data.set(key, value); this.timestamps.set(key, timestamp); return true; } return false; // Conflict - existing value is newer } get(key) { return this.data.get(key); } delete(key, timestamp = Date.now()) { const existingTimestamp = this.timestamps.get(key) || 0; if (timestamp >= existingTimestamp) { this.data.delete(key); this.timestamps.delete(key); return true; } return false; } getAll() { return Object.fromEntries(this.data.entries()); } merge(other) { for (const [key, value] of other.data.entries()) { const timestamp = other.timestamps.get(key) || 0; this.set(key, value, timestamp); } } serialize() { return JSON.stringify({ data: Object.fromEntries(this.data.entries()), timestamps: Object.fromEntries(this.timestamps.entries()) }); } static deserialize(serialized) { const parsed = JSON.parse(serialized); const crdt = new ChartDataCRDT(); crdt.data = new Map(Object.entries(parsed.data)); crdt.timestamps = new Map(Object.entries(parsed.timestamps)); return crdt; } } /** * Conflict resolution strategies for collaborative editing */ export const ConflictResolution = { /** * Last-writer-wins strategy */ lastWriterWins: (local, remote, localTime, remoteTime) => { return remoteTime > localTime ? remote : local; }, /** * Merge arrays by concatenating and removing duplicates */ mergeArrays: (local, remote) => { const merged = [...local, ...remote]; return merged.filter((item, index, arr) => arr.findIndex(i => JSON.stringify(i) === JSON.stringify(item)) === index); }, /** * Merge objects by combining properties */ mergeObjects: (local, remote) => { return { ...local, ...remote }; } }; /** * Presence awareness utilities */ export class PresenceManager { constructor() { Object.defineProperty(this, "users", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "callbacks", { enumerable: true, configurable: true, writable: true, value: new Set() }); } addUser(user) { this.users.set(user.id, { ...user, lastActive: Date.now() }); this.notifyCallbacks(); } updateUser(userId, updates) { const existing = this.users.get(userId); if (existing) { this.users.set(userId, { ...existing, ...updates, lastActive: Date.now() }); this.notifyCallbacks(); } } removeUser(userId) { this.users.delete(userId); this.notifyCallbacks(); } getUsers() { return Array.from(this.users.values()); } getActiveUsers(timeoutMs = 30000) { const now = Date.now(); return this.getUsers().filter(user => now - user.lastActive < timeoutMs); } subscribe(callback) { this.callbacks.add(callback); return () => this.callbacks.delete(callback); } notifyCallbacks() { const users = this.getUsers(); this.callbacks.forEach(callback => callback(users)); } cleanupInactiveUsers(timeoutMs = 60000) { const now = Date.now(); const inactiveUsers = Array.from(this.users.entries()) .filter(([_, user]) => now - user.lastActive > timeoutMs) .map(([id]) => id); inactiveUsers.forEach(id => this.removeUser(id)); } } /** * Annotation synchronization utilities */ export class AnnotationSync { constructor() { Object.defineProperty(this, "annotations", { enumerable: true, configurable: true, writable: true, value: new Map() }); } addAnnotation(annotation) { this.annotations.set(annotation.id, annotation); } updateAnnotation(id, updates) { const existing = this.annotations.get(id); if (existing) { this.annotations.set(id, { ...existing, ...updates }); return true; } return false; } removeAnnotation(id) { return this.annotations.delete(id); } getAnnotations() { return Array.from(this.annotations.values()); } getAnnotationsByUser(userId) { return this.getAnnotations().filter(annotation => annotation.userId === userId); } getAnnotationsByType(type) { return this.getAnnotations().filter(annotation => annotation.type === type); } clearUserAnnotations(userId) { const userAnnotations = this.getAnnotationsByUser(userId); userAnnotations.forEach(annotation => this.removeAnnotation(annotation.id)); } serialize() { return JSON.stringify(Array.from(this.annotations.values())); } deserialize(serialized) { const annotations = JSON.parse(serialized); this.annotations.clear(); annotations.forEach(annotation => this.addAnnotation(annotation)); } } /** * Connection quality monitoring */ export class ConnectionMonitor { constructor() { Object.defineProperty(this, "latencyHistory", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "maxHistorySize", { enumerable: true, configurable: true, writable: true, value: 10 }); Object.defineProperty(this, "callbacks", { enumerable: true, configurable: true, writable: true, value: new Set() }); } recordLatency(latency) { this.latencyHistory.push(latency); if (this.latencyHistory.length > this.maxHistorySize) { this.latencyHistory.shift(); } this.notifyCallbacks(); } getAverageLatency() { if (this.latencyHistory.length === 0) return 0; const sum = this.latencyHistory.reduce((a, b) => a + b, 0); return sum / this.latencyHistory.length; } getConnectionQuality() { const avgLatency = this.getAverageLatency(); if (avgLatency < 100) return 'good'; if (avgLatency < 500) return 'fair'; return 'poor'; } subscribe(callback) { this.callbacks.add(callback); return () => this.callbacks.delete(callback); } notifyCallbacks() { const quality = this.getConnectionQuality(); this.callbacks.forEach(callback => callback(quality)); } } /** * Operational Transform (OT) utilities for text-based annotations */ export class TextOperationTransform { static transform(op1, op2) { // Simplified OT implementation for text operations // In production, use a more robust OT library like ShareJS if (op1.type === 'insert' && op2.type === 'insert') { if (op1.position <= op2.position) { return [op1, { ...op2, position: op2.position + op1.text.length }]; } else { return [{ ...op1, position: op1.position + op2.text.length }, op2]; } } if (op1.type === 'delete' && op2.type === 'delete') { if (op1.position + op1.length <= op2.position) { return [op1, { ...op2, position: op2.position - op1.length }]; } else if (op2.position + op2.length <= op1.position) { return [{ ...op1, position: op1.position - op2.length }, op2]; } // Overlapping deletes - need more complex handling } // More cases would be handled in a complete implementation return [op1, op2]; } } /** * Export utilities for collaborative sessions */ export function exportCollaborativeSession(data, annotations, users, metadata) { const exportData = { version: '1.0', timestamp: Date.now(), data, annotations: annotations.map(annotation => ({ ...annotation, // Convert timestamps to ISO strings for better readability timestamp: new Date(annotation.timestamp).toISOString() })), users: users.map(user => ({ ...user, lastActive: new Date(user.lastActive).toISOString() })), metadata, stats: { totalDataPoints: data.length, totalAnnotations: annotations.length, activeUsers: users.length, sessionDuration: metadata.endTime ? metadata.endTime - metadata.startTime : null } }; return JSON.stringify(exportData, null, 2); }