import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } // relativeTime renders a compact "Xs/Xm/Xh/Xd ago" label for freshness // indicators (health checks, live event timestamps, etc). export function relativeTime(iso: string | null | undefined): string { if (!iso) return "never"; const ms = Date.now() - new Date(iso).getTime(); if (ms < 0) return "just now"; const s = Math.floor(ms / 1000); if (s < 60) return `${s}s ago`; const m = Math.floor(s / 60); if (m < 60) return `${m}m ago`; const h = Math.floor(m / 60); if (h < 24) return `${h}h ago`; const d = Math.floor(h / 24); return `${d}d ago`; } // Truncates long slugs/names in the middle (keeping the "type:" prefix and // the tail visible) rather than at the end — for slugs the distinguishing // part is often at both ends, e.g. "investigation:nomos/dragonfly-memlock-…" // vs "…rlimit-type-8-in-unprivileged-lxcs". export function truncateMiddle(s: string, maxLen = 36): string { if (s.length <= maxLen) return s; const keep = Math.floor((maxLen - 1) / 2); return `${s.slice(0, keep)}…${s.slice(-keep)}`; } // debounce wraps fn so rapid calls (e.g. keystrokes in a filter input) // collapse into one invocation after `wait`ms of silence. export function debounce void>(fn: T, wait = 300): T { let timer: ReturnType | undefined; return ((...args: Parameters) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), wait); }) as T; } // eslint-disable-next-line @typescript-eslint/no-explicit-any export type WithoutChild = T extends { child?: any } ? Omit : T; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type WithoutChildren = T extends { children?: any } ? Omit : T; export type WithoutChildrenOrChild = WithoutChildren>; export type WithElementRef = T & { ref?: U | null };