UNPKG

react-classmate

Version:

A react tool to separate class name logic, create variants and manage styles.

337 lines (328 loc) 15.2 kB
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 };