cvb
Version:
Class Variance Builder 👷🏼
210 lines • 7.75 kB
TypeScript
export type ClassValue = string | boolean | null | undefined;
export type ClassDictionary = Record<string, any>;
export type Pretty<T> = {
[K in keyof T]: T[K];
} & {};
type StringToBoolean<T> = T extends 'true' | 'false' ? boolean : T;
type OneOrMore<T> = T | Array<T>;
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
type OmitUndefined<T> = T extends undefined ? never : T;
/**
* Extract the variant as optional props from a `cvb` or `svb` function.
* Intended to be used with a JSX component.
*/
export type VariantProps<Component extends (...args: any) => any> = Omit<OmitUndefined<Parameters<Component>[0]>, 'class' | 'className'>;
export interface Compose {
<T extends ReturnType<RecipeCreatorFn>[]>(...components: [...T]): (props?: (UnionToIntersection<{
[K in keyof T]: VariantProps<T[K]>;
}[number]> | undefined) & ClassProp) => string;
}
export interface CX {
(...inputs: ClassValue[]): string;
}
export type CXOptions = Parameters<CX>;
export type CXReturn = ReturnType<CX>;
export type ClassProp = {
class?: ClassValue;
className?: never;
} | {
class?: never;
className?: ClassValue;
};
export type RecipeVariantRecord = Record<any, Record<any, ClassValue>>;
export type RecipeSelection<T extends RecipeVariantRecord | SlotRecipeVariantRecord<string>> = {
[K in keyof T]?: StringToBoolean<keyof T[K]> | undefined;
};
export type RecipeVariantFn<T extends RecipeVariantRecord> = (props?: RecipeSelection<T> & ClassProp) => string;
export type RecipeCompoundSelection<T> = {
[K in keyof T]?: OneOrMore<StringToBoolean<keyof T[K]>> | undefined;
};
export type RecipeCompoundVariant<T> = T & ClassProp;
export interface RecipeDefinition<T extends RecipeVariantRecord = RecipeVariantRecord> {
/**
* Base styles applied to every instance of the component, regardless of variant selection.
* These form the foundation of your component's appearance.
*
* @example
* base: 'flex items-center justify-center rounded-md font-medium transition-colors',
*/
base?: ClassValue;
/**
* A collection of variant groups, each containing mutually exclusive styling options.
* Variants allow your component to adapt to different contexts and requirements.
*
* @example
* variants: {
* size: {
* sm: 'text-sm px-2 py-1',
* md: 'text-base px-4 py-2',
* },
* color: {
* primary: 'bg-blue-500 text-white hover:bg-blue-600',
* secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300'
* },
* rounded: {
* true: 'rounded-full',
* false: 'rounded-md'
* }
* }
*/
variants?: T;
/**
* Styles applied when specific combinations of variants are selected.
* This enables complex conditional styling based on multiple variant selections.
*
* @example
* compoundVariants: [
* {
* // Apply when size is small and color is primary
* size: 'sm',
* color: 'primary',
* class: 'uppercase tracking-wider'
* },
* {
* // Apply when size is either md or lg and rounded is true
* size: ['md', 'lg'],
* rounded: true,
* class: 'shadow-md'
* }
* ]
*/
compoundVariants?: Pretty<RecipeCompoundVariant<RecipeCompoundSelection<T>>>[];
/**
* Predefined variant selections to apply when no specific variants are provided.
* These act as the component's factory settings for a consistent default appearance.
*
* @example
* defaultVariants: {
* size: 'md',
* color: 'primary',
* rounded: false
* }
*/
defaultVariants?: RecipeSelection<T>;
}
export type RecipeCreatorFn = <T extends RecipeVariantRecord>(config: RecipeDefinition<T>) => RecipeVariantFn<T>;
export type SlotRecord<S extends string, T> = Partial<Record<S, T>>;
export type SlotRecipeVariantRecord<S extends string> = Record<any, Record<any, SlotRecord<S, ClassValue>>>;
export type SlotRecipeVariantFn<S extends string, T extends SlotRecipeVariantRecord<S>> = (props?: RecipeSelection<T> & SlotClassProp<S>) => SlotRecord<S, string>;
export type SlotClassProp<S extends string> = {
class?: SlotRecord<S, ClassValue>;
className?: never;
} | {
class?: never;
className?: SlotRecord<S, ClassValue>;
};
export type SlotRecipeCompoundVariant<S extends string, T> = T & SlotClassProp<S>;
export interface SlotRecipeDefinition<S extends string = string, T extends SlotRecipeVariantRecord<S> = SlotRecipeVariantRecord<S>> {
/**
* An array of named slots that make up the component.
* Each slot represents a distinct part of the component that can be styled independently.
*
* @example
* slots: ['root', 'header', 'body']
*/
slots: S[] | Readonly<S[]>;
/**
* Base styles applied to each slot, regardless of any variants selected.
* These styles form the foundation of your component's appearance.
*
* @example
* base: {
* root: 'flex flex-col border rounded-lg overflow-hidden',
* header: 'p-4 font-semibold border-b bg-gray-50',
* body: 'p-4',
* }
*/
base?: SlotRecord<S, ClassValue>;
/**
* Multi-variant styles organized by variant groups and options.
* Each variant option contains styles for specific slots, allowing different
* parts of your component to respond to the same variant changes.
*
* @example
* variants: {
* size: {
* sm: { root: 'text-sm', header: 'py-2' },
* lg: { root: 'text-lg', header: 'py-4' }
* },
* rounded: {
* true: { root: 'rounded-full' }
* }
* }
*/
variants?: T;
/**
* Conditional styles that apply only when a specific combination of variants is selected.
* This enables complex styling logic that depends on multiple variant selections.
*
* @example
* compoundVariants: [
* {
* size: 'sm',
* rounded: true,
* class: { root: 'shadow-sm', header: 'uppercase' }
* },
* {
* size: ['md', 'lg'],
* rounded: false,
* class: { root: 'shadow-md' }
* }
* ]
*/
compoundVariants?: Pretty<SlotRecipeCompoundVariant<S, RecipeCompoundSelection<T>>>[];
/**
* Predefined variant selections to apply when no specific variants are provided.
* These act as the component's factory settings, ensuring consistent appearance
* even without explicit configuration.
*
* @example
* defaultVariants: {
* size: 'md',
* rounded: false
* }
*/
defaultVariants?: RecipeSelection<T>;
}
export type SlotRecipeCreatorFn = <S extends string, T extends SlotRecipeVariantRecord<S>>(config: SlotRecipeDefinition<S, T>) => SlotRecipeVariantFn<S, T>;
export interface DefineConfigOptions {
hooks?: {
/**
* A hook function called after class names are concatenated but before they're returned.
* This enables post-processing of the final class string, such as deduplication or
* resolving class conflicts.
*
* @param className - The fully concatenated class string generated by CVB functions
* @returns The processed class string that will be returned to the caller
*/
onComplete?: (className: string) => string;
};
}
export interface DefineConfig {
(options?: DefineConfigOptions): {
compose: Compose;
cx: CX;
cvb: RecipeCreatorFn;
svb: SlotRecipeCreatorFn;
};
}
export {};
//# sourceMappingURL=types.d.ts.map