react-classmate
Version:
A react tool to separate class name logic, create variants and manage styles.
337 lines (328 loc) • 15.2 kB
TypeScript
import * as tailwind_merge from 'tailwind-merge';
import { CSSProperties, ForwardRefExoticComponent, PropsWithoutRef, RefAttributes, JSXElementConstructor, JSX, DependencyList } from 'react';
/**
* Converts props to their `$`-prepended counterparts and removes the original keys.
* Mainly used if you wanna "mirror" properties from a component to a classmate component.
*
* @param props - The original props object.
* @param mappings - An object mapping original keys (from BaseProps) to `$`-prepended keys.
* @returns A new object with `$`-prepended keys and original keys removed.
* @example
* ```tsx
* const preparedProps = convertRcProps(buttonProps, {
* size: "$size",
* noShadow: "$noShadow",
* noGutter: "$noGutter",
* loading: "$loading",
* disabled: "$disabled",
* color: "$color",
* })
* ```
* will result in (example):
* ```tsx
* const preparedProps = {
* $size: "md",
* $color: "primary",
* $disabled: false,
* $loading: false,
* $noShadow: false,
* $noGutter: false,
* }
*/
declare const convertRcProps: <T extends object, BaseProps extends object, K extends keyof BaseProps & keyof T>(props: T, mappings: Record<K, `$${K & string}`>) => Omit<T, K> & Record<string, any>;
/**
* Interpolation type for "styled components".
*
* Interpolations can be:
* - Static strings or booleans.
* - Functions that take the component's props and return a class name string.
* - Null or undefined values (ignored in class name computation).
*
* @typeParam T - The type of the props passed to the interpolation function.
*/
type InterpolationBase<T> = string | boolean | ((props: T) => string) | null | undefined;
type Interpolation<T> = InterpolationBase<T & {
style: (styleDef: StyleDefinition<T>) => string;
}>;
type LogicHandler<P extends object> = (props: P) => Partial<P> | undefined;
type InputComponent = ForwardRefExoticComponent<any> | JSXElementConstructor<any> | RcBaseComponent<any>;
/**
* Base type for styled React components with forward refs.
*
* @typeParam P - Props of the component.
*/
interface RcBaseComponent<P extends object = object> extends ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<any>> {
__rcComputeClassName?: (props: P) => string;
__rcTag?: keyof React.JSX.IntrinsicElements | JSXElementConstructor<any>;
__rcStyles?: StyleDefinition<P> | ((props: P) => StyleDefinition<P>);
__rcLogic?: LogicHandler<P>[];
}
/**
* The `extend` method allows you to create a new styled component from an existing one.
*
* @typeParam E - The type of the original component, which can be a ForwardRefExoticComponent or a JSXElementConstructor.
* @param component - The base component to extend.
* @returns A function that accepts template strings and interpolations, and returns a new styled component.
* @example
* ```tsx
* // Extending a custom component without intrinsic element type
* const SomeBase = rc.div<{ $active?: boolean }>`color: red;`
* const Extended = rc.extend(SomeBase)<{ $highlighted?: boolean }>`
* ${p => p.$highlighted ? 'bg-yellow' : ''}
* ${p => p.$active ? 'text-red' : ''}
* `
*
* // Extending with specific props:
* const ExtendedButton = rc.extend(StyledButton)<ButtonHTMLAttributes<HTMLButtonElement>>`
* ${p => p.type === 'submit' ? 'font-bold' : ''}
* ```
*/
type ExtendFunction =
/**
* The `extend` method allows you to create a new styled component from an existing one.
*
* @typeParam E - The type of the original component, which can be a ForwardRefExoticComponent or a JSXElementConstructor.
* @param component - The base component to extend.
* @returns A function that accepts template strings and interpolations, and returns a new styled component.
* @example
* ```tsx
* // Extending a custom component without intrinsic element type
* const SomeBase = rc.div<{ $active?: boolean }>`color: red;`
* const Extended = rc.extend(SomeBase)<{ $highlighted?: boolean }>`
* ${p => p.$highlighted ? 'bg-yellow' : ''}
* ${p => p.$active ? 'text-red' : ''}
* `
*
* // Extending with specific props:
* const ExtendedButton = rc.extend(StyledButton)<ButtonHTMLAttributes<HTMLButtonElement>>`
* ${p => p.type === 'submit' ? 'font-bold' : ''}
* ```
*/
<E extends InputComponent, I extends keyof JSX.IntrinsicElements>(component: E) => ExtendTemplateBuilder<E, I>;
interface ExtendTemplateBuilder<E extends InputComponent, I extends keyof JSX.IntrinsicElements, LogicProps extends object = object> {
<T extends object>(strings: TemplateStringsArray, ...interpolations: Interpolation<MergeProps<E, T> & JSX.IntrinsicElements[I]>[]): RcBaseComponent<MergeProps<E, T>>;
logic<NextLogic extends object = object>(handler: LogicHandler<MergeProps<E, LogicProps & NextLogic>>): ExtendTemplateBuilder<E, I, LogicProps & NextLogic>;
}
/**
* Base type for the base classes in the variants configuration.
*
* This can be a static string or a function that returns a string based on the component's props.
*
* @typeParam VariantProps - The props for the variants.
* @typeParam ExtraProps - Additional props for the component.
*/
type VariantsConfigBase<VariantProps, ExtraProps> = string | ((props: VariantProps & ExtraProps & {
style: (styleDef: StyleDefinition<VariantProps & ExtraProps>) => string;
}) => string);
/**
* Type for the variants object in the variants configuration.
*
* The keys are the prop names, and the values are objects with class names or functions that return class names.
*
* @typeParam VariantProps - The props for the variants.
* @typeParam ExtraProps - Additional props for the component.
*/
type VariantsConfigVariants<VariantProps, ExtraProps> = {
[Key in keyof VariantProps]?: Record<string, string | ((props: VariantProps & ExtraProps & {
style: (styleDef: StyleDefinition<VariantProps & ExtraProps>) => string;
}) => string)>;
};
/**
* Configuration object for creating styled components with variants.
*
* @typeParam VariantProps - The props for the variants.
* @typeParam ExtraProps - Additional props for the component.
*/
type VariantsConfig<VariantProps extends object, ExtraProps extends object> = {
/**
* The base classes for the styled component.
* This can be a static string or a function that returns a string based on the component's props.
* If not provided, the base classes are empty.
*/
base?: VariantsConfigBase<VariantProps, ExtraProps>;
/**
* The variants object defines the classes for each prop value.
* The keys are the prop names, and the values are objects with class names or functions that return class names.
*/
variants: VariantsConfigVariants<VariantProps, ExtraProps>;
/**
* Default variants to apply if a variant prop is not passed.
* For example, if you have a variant `size` and a default variant value of `md`,
* it will use `md` if no explicit `size` prop is provided.
*/
defaultVariants?: Partial<Record<keyof VariantProps, string>>;
};
type VariantsFunction<K> =
/**
* The variants function allows you to create a styled component with variants.
*
* @param config - The configuration object for creating variants.
* @returns A styled component with variants based on the configuration object.
* @example
* ```tsx
* interface AlertProps {
* $isActive?: boolean;
* }
* // You can additionally type the variant props for strict type checking
* interface AlertVariants {
* $severity: "info" | "warning" | "error";
* }
*
* const Alert = rc.div.variants<AlertProps, AlertVariants>({
* base: p => `${p.$isActive ? "pointer-cursor" : ""} p-4 rounded-md`,
* variants: {
* $severity: {
* info: (p) => `bg-blue-100 text-blue-800 ${p.$isActive ? "shadow-lg" : ""}`,
* warning: (p) => `bg-yellow-100 text-yellow-800 ${p.$isActive ? "font-bold" : ""}`,
* error: (p) => `bg-red-100 text-red-800 ${p.$isActive ? "ring ring-red-500" : ""}`,
* },
* },
* });
*
* export default () => <Alert $severity="info" $isActive />
* // Outputs: <div className="custom-active p-4 rounded-md bg-blue-100 text-blue-800 shadow-lg" />
* ```
*/
<ExtraProps extends object, VariantProps extends object = ExtraProps>(config: VariantsConfig<VariantProps, ExtraProps>) => RcBaseComponent<MergeProps<K, ExtraProps & Partial<VariantProps>>>;
/**
* Factory for creating styled components with intrinsic elements.
*/
interface RcFactoryFunction<K extends keyof JSX.IntrinsicElements> {
<T extends object>(strings: TemplateStringsArray, ...interpolations: Interpolation<T>[]): RcBaseComponent<MergeProps<K, T>>;
logic<LogicProps extends object = object>(handler: LogicHandler<MergeProps<K, LogicProps>>): RcFactoryFunction<K>;
variants: VariantsFunction<K>;
}
type RcComponentFactory = {
[K in keyof JSX.IntrinsicElements]: RcFactoryFunction<K>;
} & {
extend: ExtendFunction;
};
/**
* Extracts the inner props of a component.
*
* If `P` is a component with `PropsWithoutRef` and `RefAttributes`, the props are extracted.
* Otherwise, `P` is returned as is.
*
* @typeParam P - The type of the component to extract props from.
*/
type InnerProps<P> = P extends PropsWithoutRef<infer U> & RefAttributes<any> ? U : P;
/**
* Merges additional props with the base props of a given component or intrinsic element.
*
* @typeParam E - The base component type or intrinsic element.
* @typeParam T - Additional props to merge with the base props.
*/
type MergeProps<E, T> = E extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[E] & T : E extends ForwardRefExoticComponent<infer P> ? InnerProps<P> & T : E extends JSXElementConstructor<infer P2> ? P2 & T : T;
type StaticStyleValue = string | number;
type DynamicStyleValue<P> = (props: P) => StaticStyleValue;
type StyleDefinition<P> = {
[Key in keyof CSSProperties]?: StaticStyleValue | DynamicStyleValue<P>;
};
type AllowedTags = (typeof domElements)[number];
declare const domElements: readonly ["a", "abbr", "address", "area", "article", "aside", "audio", "b", "base", "bdi", "bdo", "big", "blockquote", "body", "br", "button", "canvas", "caption", "cite", "code", "col", "colgroup", "data", "datalist", "dd", "del", "details", "dfn", "dialog", "div", "dl", "dt", "em", "embed", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "iframe", "img", "input", "ins", "kbd", "keygen", "label", "legend", "li", "link", "main", "map", "mark", "menu", "menuitem", "meta", "meter", "nav", "noscript", "object", "ol", "optgroup", "option", "output", "p", "param", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "script", "section", "select", "small", "source", "span", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "u", "ul", "var", "video", "wbr", "circle", "clipPath", "defs", "ellipse", "foreignObject", "g", "image", "line", "linearGradient", "marker", "mask", "path", "pattern", "polygon", "polyline", "radialGradient", "rect", "stop", "svg", "text", "tspan", "circle", "clipPath", "defs", "ellipse", "foreignObject", "g", "image", "line", "linearGradient", "marker", "mask", "path", "pattern", "polygon", "polyline", "radialGradient", "rect", "stop"];
interface CreateVariantMapOptions<T extends AllowedTags> {
elements: readonly T[];
variantsConfig: VariantsConfig<any, any>;
}
/**
* Generates a map of variant components based on the provided elements and variants configuration.
* Mainly used for creating multiple variant components at once.
*
* @param options - An object containing the elements and variants configuration.
* @returns A record mapping each element name to its corresponding variant component.
* @example
* ```tsx
* const button = createVariantMap({
* elements: ["button", "a"],
* variantsConfig: buttonVariants,
* })
* ```
* will result in:
* ```tsx
* const button = {
* button: RcBaseComponent<any>, // rc.button.variants(buttonVariants)
* a: RcBaseComponent<any>, // rc.a.variants(buttonVariants)
* }
*/
declare const createVariantMap: <T extends AllowedTags>({ elements, variantsConfig, }: CreateVariantMapOptions<T>) => Record<T, RcBaseComponent<any>>;
/**
* Memoizes a classmate component factory within a React component.
*
* Useful when you need to declare a classmate component inside another component
* but want to avoid re-instantiating it on every render. Pass the values your factory
* depends on via the `deps` array to recompute when needed.
*
* @example
* ```tsx
* const Component = ({ $status }: { $status: "info" | "warning" }) => {
* const StyledAlert = useClassmate(
* () =>
* rc.div.variants({
* base: "p-4 rounded",
* variants: {
* $status: {
* info: "bg-blue-100 text-blue-900",
* warning: "bg-yellow-100 text-yellow-900",
* },
* },
* }),
* [], // recompute only if the factory dependencies change
* )
*
* return <StyledAlert $status={$status}>Content</StyledAlert>
* }
* ```
*/
declare const useClassmate: <Props extends object>(factory: () => RcBaseComponent<Props>, deps?: DependencyList) => RcBaseComponent<Props>;
/**
* The `rc` instance is the main entry point for creating our classmate-components.
* It provides:
* - Component builder to create classmate components by using template literals and interpolations. E.g: `rc.div` or `rc.button`
* - A variants method to create classmate components with variants. E.g: `rc.div.variants(...)`
* - The `rc.extend` method that allows you to create new classmate components based on existing ones.
*
* Each styled component created via `rc` filters out `$`-prefixed props from the DOM and computes a final `className`
* string by combining user-defined classes, dynamic interpolations based on props, and any incoming `className`.
*
* @example
* ```tsx
* // simple usage:
* const StyledDiv = rc.div`p-2`
*
* // Creating a styled 'div' with conditional classes:
* const StyledDiv = rc.div<{ $active?: boolean }>`
* p-2
* ${p => p.$active ? 'bg-blue' : 'bg-green'}
* `
*
* // Using the styled component:
* <StyledDiv $active>Active Content</StyledDiv>
*
* // Extending an existing styled component:
* const ExtendedDiv = rc.extend(StyledDiv)<{ $highlighted?: boolean }>`
* ${p => p.$highlighted ? 'border-2 border-yellow' : ''}
* `
*
* // Validating props against an intrinsic element:
* const ExtendedButton = rc.extend(rc.button)`
* ${p => p.type === 'submit' ? 'font-bold' : ''}
* `
*
* // Creating a styled component with variants:
* const StyledButton = rc.button.variants({
* base: 'p-2',
* variants: {
* size: {
* sm: 'p-1',
* lg: 'p-3',
* },
* defaultVariants: {
* size: 'sm',
* },
* })
* ```
*/
declare const rc: RcComponentFactory;
/** the `twMerge` lib from react-classmate */
declare const rcMerge: (...classLists: tailwind_merge.ClassNameValue[]) => string;
export { type RcBaseComponent, type VariantsConfig, convertRcProps, createVariantMap, rc as default, rcMerge, useClassmate };