/** * Variant-driven class builder — the kit's local replacement for * `class-variance-authority`. * * A component declares its visual options once as a table, and the returned * function turns a set of props into the matching class string: * * ```ts * const badgeVariants = cva('inline-flex rounded-md', { * variants: { tone: { default: 'bg-primary', muted: 'bg-muted' } }, * defaultVariants: { tone: 'default' }, * }); * * badgeVariants({ tone: 'muted' }) // 'inline-flex rounded-md bg-muted' * badgeVariants({ tone: null }) // 'inline-flex rounded-md' ← opts out * ``` * * The prop/return contract matches what `class-variance-authority` shipped, so * `VariantProps` keeps deriving component prop types the same way. */ import { cx, type ClassValue } from './cx'; /** * Both spellings are accepted and appended last, after the variant classes, so * a caller's own class always has the final say. */ export interface ClassProp { class?: ClassValue; className?: ClassValue; } /** A boolean variant is declared as `{ true: '…', false: '…' }`. */ type StringToBoolean = T extends 'true' | 'false' ? boolean : T; type ConfigSchema = Record>; /** `null` opts a variant out entirely — its default is not substituted. */ type ConfigVariants = { [Variant in keyof T]?: StringToBoolean | null | undefined; }; /** A compound entry may match several values of the same variant at once. */ type ConfigVariantsMulti = { [Variant in keyof T]?: | StringToBoolean | StringToBoolean[] | undefined; }; type Config = T extends ConfigSchema ? { variants?: T; defaultVariants?: ConfigVariants; /** Classes applied only when every listed variant matches at once. */ compoundVariants?: ((ConfigVariants | ConfigVariantsMulti) & ClassProp)[]; } : never; type Props = T extends ConfigSchema ? ConfigVariants & ClassProp : ClassProp; type OmitUndefined = T extends undefined ? never : T; /** * The variant props of a `cva()` function, so a component's public prop type is * derived from its variant table instead of restated beside it: * * ```ts * interface ButtonProps * extends React.ComponentProps<'button'>, * VariantProps {} * ``` */ export type VariantProps unknown> = Omit< OmitUndefined[0]>, 'class' | 'className' >; /** The generic `Config` is a conditional type; the runtime reads this shape. */ interface RuntimeConfig { variants?: ConfigSchema; defaultVariants?: Record; compoundVariants?: (Record & ClassProp)[]; } /** * Normalises a variant value into a table key. * * `true`/`false` index `variants.x.true` / `.false`, and `0` indexes `'0'` — * without this both would be treated as "not provided" and silently fall back * to the default. */ const toKey = (value: unknown): unknown => typeof value === 'boolean' ? String(value) : value === 0 ? '0' : value; export function cva(base?: ClassValue, config?: Config) { return (props?: Props): string => { const runtime = config as RuntimeConfig | undefined; const variants = runtime?.variants; const given = props as Record | undefined; if (!variants) return cx(base, given?.class as ClassValue, given?.className as ClassValue); const defaults = runtime?.defaultVariants; const variantClasses = Object.keys(variants).map((name) => { const value = given?.[name]; /* An explicit `null` strips the variant, default included — that is how a caller renders the bare base. `undefined` still falls back. */ if (value === null) return undefined; const key = toKey(value) || toKey(defaults?.[name]); return key == null ? undefined : variants[name][String(key)]; }); /* Compound matching sees defaults plus whatever was actually passed. A prop explicitly set to `undefined` must not shadow the default it falls back to, so undefined entries are skipped rather than assigned. */ const active: Record = { ...defaults }; if (given) { for (const [key, value] of Object.entries(given)) { if (value !== undefined) active[key] = value; } } const compoundClasses = runtime?.compoundVariants?.map( ({ class: compoundClass, className: compoundClassName, ...match }) => { const matches = Object.entries(match).every(([key, expected]) => Array.isArray(expected) ? expected.includes(active[key]) : active[key] === expected ); return matches ? [compoundClass, compoundClassName] : undefined; } ); return cx( base, variantClasses, compoundClasses, given?.class as ClassValue, given?.className as ClassValue ); }; }