react-class-variants
Version:
Type-safe React variants API for dynamic CSS class composition
213 lines (210 loc) • 9.32 kB
text/typescript
import { ElementType, ComponentPropsWithRef, HTMLAttributes, Ref, ReactNode, ReactElement, JSX } from 'react';
type PickRequiredKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];
type OmitByValue<T, Value> = {
[P in keyof T as T[P] extends Value ? never : P]: T[P];
};
type StringToBoolean<T> = T extends 'true' | 'false' ? boolean : T;
type Simplify<T> = {
[K in keyof T]: T[K];
};
type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
type Exact<T, Shape> = T extends Shape ? Exclude<keyof T, keyof Shape> extends never ? T : never : never;
type ClassNameValue = string | null | undefined | ClassNameValue[];
/**
* Definition of the available variants and their options.
* @example
* {
* color: {
* white: "bg-white"
* green: "bg-green-500",
* },
* size: {
* small: "text-xs",
* large: "text-lg"
* }
* }
*/
type VariantsSchema = Record<string, Record<string, ClassNameValue>>;
type VariantsConfig<V extends VariantsSchema> = {
base?: ClassNameValue;
variants?: V;
defaultVariants?: keyof V extends never ? Record<string, never> : Partial<Variants<V>>;
compoundVariants?: keyof V extends never ? never[] : CompoundVariant<V>[];
};
/**
* Rules for class names that are applied for certain variant combinations.
*/
interface CompoundVariant<V extends VariantsSchema> {
variants: Partial<VariantsMulti<V>>;
className: ClassNameValue;
}
/**
* Maps variant names to their possible values (with boolean string conversion).
*/
type Variants<V extends VariantsSchema> = {
[Variant in keyof V]: StringToBoolean<keyof V[Variant]>;
};
/**
* Like Variants but allows arrays of values for compound variant matching.
*/
type VariantsMulti<V extends VariantsSchema> = {
[Variant in keyof V]: StringToBoolean<keyof V[Variant]> | StringToBoolean<keyof V[Variant]>[];
};
/**
* Only the boolean variants, i.e. ones that have "true" or "false" as options.
*/
type BooleanVariants<C extends VariantsConfig<V>, V extends VariantsSchema = NonNullable<C['variants']>> = {
[Variant in keyof V as V[Variant] extends {
true: any;
} | {
false: any;
} ? Variant : never]: V[Variant];
};
/**
* Only the variants for which a default options is set.
*/
type DefaultVariants<C extends VariantsConfig<V>, V extends VariantsSchema = NonNullable<C['variants']>> = {
[Variant in keyof V as Variant extends keyof OmitByValue<C['defaultVariants'], undefined> ? Variant : never]: V[Variant];
};
/**
* Names of all optional variants, i.e. booleans or ones with default options.
*/
type OptionalVariantNames<C extends VariantsConfig<V>, V extends VariantsSchema = NonNullable<C['variants']>> = keyof BooleanVariants<C, V> | keyof DefaultVariants<C, V>;
type VariantOptions<C extends VariantsConfig<V>, V extends VariantsSchema = NonNullable<C['variants']>> = keyof V extends never ? {} : Required<Omit<Variants<V>, OptionalVariantNames<C, V>>> & Partial<Pick<Variants<V>, OptionalVariantNames<C, V>>>;
interface VariantFactoryOptions {
onClassesMerged?: (className: string) => string;
}
type VariantsResolverArgs<P> = PickRequiredKeys<P> extends never ? [props?: P] : [props: P];
/**
* Type for the variants resolver function with config metadata.
*/
type VariantsResolverFn<C extends VariantsConfig<V>, V extends VariantsSchema> = ((...args: VariantsResolverArgs<{
className?: ClassNameValue;
} & Omit<VariantOptions<C, V>, 'className'>>) => string) & {
/**
* @internal
* Type-only property to store configuration for type extraction.
* This property does not exist at runtime.
*/
__config?: C;
};
/**
* Type for the variant props resolver function with config metadata.
*/
type VariantPropsResolverFn<C extends VariantComponentConfig<V>, V extends VariantsSchema> = (<P extends Omit<VariantOptions<C, V>, 'className'> & {
className?: string;
}>(props: P) => {
className: string;
} & Omit<P, 'className' | '__config' | (C['forwardProps'] extends (keyof V)[] ? Exclude<keyof V, C['forwardProps'][number]> : keyof V)>) & {
/**
* @internal
* Type-only property to store configuration for type extraction.
* This property does not exist at runtime.
*/
__config?: C;
};
/**
* Configuration for variant component with optional render prop control.
*/
type VariantComponentConfig<V extends VariantsSchema> = VariantsConfig<V> & {
withoutRenderProp?: boolean;
forwardProps?: (keyof V)[];
};
/**
* Base props for a variant component, combining variant options with component props.
*/
type BaseVariantComponentProps<T extends ElementType, C extends VariantComponentConfig<V>, V extends VariantsSchema> = VariantOptions<C, V> & Omit<ComponentPropsWithRef<T>, keyof VariantOptions<C, V>>;
/**
* Extracts the full configuration from a variant function, resolver, or component.
* @template T - The variant function, resolver, or component type
* @returns The VariantsConfig or VariantComponentConfig type
*
* @example
* // Works with variants()
* const buttonVariants = variants({
* base: 'btn',
* variants: { color: { primary: 'bg-blue' } }
* });
* type Config1 = ExtractVariantConfig<typeof buttonVariants>;
*
* // Works with variantPropsResolver()
* const resolveProps = variantPropsResolver({ ... });
* type Config2 = ExtractVariantConfig<typeof resolveProps>;
*
* // Works with variantComponent()
* const Button = variantComponent('button', { ... });
* type Config3 = ExtractVariantConfig<typeof Button>;
*/
type ExtractVariantConfig<T> = T extends {
__config?: infer Config;
} ? Config extends VariantsConfig<any> ? Prettify<Config> : never : never;
/**
* Extracts variant options type from a variant function, resolver, or component.
* @template T - The variant function, resolver, or component type
* @returns The VariantOptions type
*
* @example
* // Works with variants()
* const buttonVariants = variants({
* variants: { color: { primary: 'bg-blue', secondary: 'bg-gray' } }
* });
* type Options1 = ExtractVariantOptions<typeof buttonVariants>;
* // { color: 'primary' | 'secondary' }
*
* // Works with variantPropsResolver()
* const resolveProps = variantPropsResolver({
* variants: { size: { small: 'text-sm', large: 'text-lg' } },
* defaultVariants: { size: 'small' }
* });
* type Options2 = ExtractVariantOptions<typeof resolveProps>;
* // { size?: 'small' | 'large' }
*
* // Works with variantComponent()
* const Button = variantComponent('button', {
* variants: { color: { primary: 'bg-blue' } }
* });
* type Options3 = ExtractVariantOptions<typeof Button>;
* // { color: 'primary' }
*/
type ExtractVariantOptions<T> = T extends {
__config?: infer Config;
} ? Config extends VariantsConfig<infer Schema> ? Prettify<VariantOptions<Config, Schema>> : never : never;
/**
* Render prop type.
* @template P Props
* @example
* const children: RenderPropFn = (props) => <div {...props} />;
*/
type RenderPropFn<P = HTMLAttributes<any> & {
ref?: Ref<any>;
}> = (props: P) => ReactNode;
type RenderPropType<C extends VariantComponentConfig<V>, V extends VariantsSchema> = RenderPropFn<Prettify<{
className: string;
ref?: Ref<any>;
} & (C['forwardProps'] extends (keyof VariantOptions<C, V>)[] ? Pick<VariantOptions<C, V>, C['forwardProps'][number]> : {}) & Omit<HTMLAttributes<any>, 'render' | 'className' | keyof VariantOptions<C, V>>>> | ReactElement;
/**
* Component props with optional render prop.
*/
type VariantComponentPropsWithRender<P, C extends VariantComponentConfig<V>, V extends VariantsSchema> = Simplify<{
render?: RenderPropType<C, V>;
} & P>;
type VariantComponentType<T extends ElementType, C extends VariantComponentConfig<V>, V extends VariantsSchema = NonNullable<C['variants']>> = (T extends keyof JSX.IntrinsicElements ? C extends {
withoutRenderProp: true;
} ? (props: BaseVariantComponentProps<T, C, V>) => ReactElement : (props: VariantComponentPropsWithRender<BaseVariantComponentProps<T, C, V>, C, V>) => ReactNode : (props: BaseVariantComponentProps<T, C, V>) => ReactElement) & {
/**
* @internal
* Type-only property to store component configuration for type extraction.
* This property does not exist at runtime.
*/
__config?: C;
};
declare function defineConfig(options?: VariantFactoryOptions): {
readonly variants: <C extends VariantsConfig<V>, V extends VariantsSchema = NonNullable<C["variants"]>>(config: Exact<Simplify<C>, VariantsConfig<V>>) => VariantsResolverFn<C, V>;
readonly variantPropsResolver: <C extends VariantComponentConfig<V>, V extends VariantsSchema = NonNullable<C["variants"]>>(config: Exact<Simplify<C>, VariantComponentConfig<V>>) => VariantPropsResolverFn<C, V>;
readonly variantComponent: <T extends ElementType, C extends VariantComponentConfig<V>, V extends VariantsSchema = NonNullable<C["variants"]>>(elementType: T, config: Exact<Simplify<C>, VariantComponentConfig<V>>) => VariantComponentType<T, C, V>;
};
export { type BaseVariantComponentProps, type ClassNameValue, type ExtractVariantConfig, type ExtractVariantOptions, type VariantComponentConfig, type VariantComponentPropsWithRender, type VariantComponentType, type VariantFactoryOptions, type VariantOptions, type VariantsConfig, type VariantsSchema, defineConfig };