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
JavaScript
// 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);
}