/**
* Shared utilities for MCP UI components.
*/
/**
* Debounce a function to only execute after a delay since the last call.
*/
export function debounce<T extends (...args: unknown[]) => void>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let timeoutId: ReturnType<typeof setTimeout>;
return (...args: Parameters<T>) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
/**
* Throttle a function to execute at most once per interval.
*/
export function throttle<T extends (...args: unknown[]) => void>(
fn: T,
interval: number
): (...args: Parameters<T>) => void {
let lastTime = 0;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
return (...args: Parameters<T>) => {
const now = Date.now();
const remaining = interval - (now - lastTime);
if (remaining <= 0) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
lastTime = now;
fn(...args);
} else if (!timeoutId) {
timeoutId = setTimeout(() => {
lastTime = Date.now();
timeoutId = null;
fn(...args);
}, remaining);
}
};
}
/**
* Generate a unique ID with optional prefix.
*/
export function uniqueId(prefix = 'mcp'): string {
return `${prefix}-${Math.random().toString(36).slice(2, 9)}`;
}
/**
* Clamp a number between min and max values.
*/
export function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
/**
* Format a number with thousands separators.
*/
export function formatNumber(num: number): string {
return num.toLocaleString();
}
/**
* Copy text to clipboard with fallback.
*/
export async function copyToClipboard(text: string): Promise<boolean> {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
// Fallback for older browsers
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
const success = document.execCommand('copy');
document.body.removeChild(textarea);
return success;
} catch {
return false;
}
}
/**
* Escape HTML special characters.
*/
export function escapeHtml(text: string): string {
const map: Record<string, string> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
return text.replace(/[&<>"']/g, (char) => map[char]);
}
/**
* Check if the user prefers reduced motion.
*/
export function prefersReducedMotion(): boolean {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
/**
* Check if the user prefers dark mode via media query.
*/
export function prefersDarkMode(): boolean {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
/**
* Simple class names utility - filters falsy values and joins.
*/
export function cx(
...classes: (string | boolean | undefined | null)[]
): string {
return classes.filter(Boolean).join(' ');
}