/** * Class-name composer — the kit's local replacement for `clsx`. * * It flattens strings, numbers, arrays and `{ class: condition }` objects into * one space-separated string, dropping anything falsy. It does **not** resolve * Tailwind conflicts; that is `twMerge`'s job and happens in `cn()`. * * ```ts * cx('px-2', isActive && 'bg-accent', { 'opacity-50': disabled }, ['a', 'b']) * ``` */ export type ClassDictionary = Record; export type ClassArray = ClassValue[]; export type ClassValue = | ClassArray | ClassDictionary | string | number | bigint | null | boolean | undefined; /** Joins with a single space, without leaving a leading one on the first item. */ const join = (accumulated: string, next: string) => accumulated ? `${accumulated} ${next}` : next; function flatten(value: ClassValue): string { if (typeof value === 'string') return value; if (typeof value === 'number' || typeof value === 'bigint') return String(value); /* Booleans, null and undefined carry no class name. Guarding here is what makes `cond && 'cls'` work without the caller filtering first. */ if (typeof value !== 'object' || value === null) return ''; let out = ''; if (Array.isArray(value)) { for (const item of value) { if (!item) continue; const resolved = flatten(item); if (resolved) out = join(out, resolved); } return out; } /* Object form: the key is the class, the value is the condition. */ for (const key in value) { if (value[key]) out = join(out, key); } return out; } export function cx(...inputs: ClassValue[]): string { let out = ''; for (const input of inputs) { if (!input) continue; const resolved = flatten(input); if (resolved) out = join(out, resolved); } return out; }