341 lines
8.9 KiB
TypeScript

import { twMerge } from "tailwind-merge";
/**
* Takes a string, and returns boolean if it is "true" or "false".
* Otherwise, returns the string.
*
* @example
* ```
* type BooleanString = BooleanString<"true" | "false" | "other">
* // BooleanString = true | false | "other" = boolean | "other"
* ```
*/
type BooleanString<T extends string> = T extends "true"
? true
: T extends "false"
? false
: T;
/**
* A type that represents a variant object.
*
* @example
* ```
* const variant: VariantType = {
* opened: {
* true: "opacity-100",
* false: "opacity-0",
* }
* size: {
* sm: "small",
* md: "medium",
* lg: "large",
* },
* color: {
* red: "#ff0000",
* green: "#00ff00",
* blue: "#0000ff",
* },
* }
* ```
*/
type VariantType = Record<string, Record<string, string>>;
/**
* Takes VariantType, and returns a type that represents the variant object.
*
* @example
* ```
* const kvs: VariantKV<VariantType> = {
* opened: true // BooleanString<"true" | "false"> = boolean;
* size: "sm" // BooleanString<"sm" | "md" | "lg"> = "sm" | "md" | "lg";
* color: "red" // BooleanString<"red" | "green" | "blue"> = "red" | "green" | "blue";
* }
* ```
*/
type VariantKV<V extends VariantType> = {
[VariantKey in keyof V]: BooleanString<keyof V[VariantKey] & string>;
};
/**
* Used for safely casting `Object.entries(<VariantKV>)`
*/
type VariantKVEntry<V extends VariantType> = [
keyof V,
BooleanString<keyof V[keyof V] & string>,
][];
/**
* Takes VariantKV as parameter, return className string.
*
* @example
* vcn({
* /* ... *\/
* dynamics: [
* ({ a, b }) => {
* return a === "something" ? "asdf" : b
* },
* ]
* })
*/
type DynamicClassName<V extends VariantType> = (
variantProps: VariantKV<V>,
) => string;
/**
* Takes VariantType, and returns a type that represents the preset object.
*
* @example
* ```
* const presets: PresetType<VariantType> = {
* preset1: {
* opened: true,
* size: "sm",
* color: "red",
* },
* preset2: {
* opened: false,
* size: "md",
* color: "green",
* className: "transition-opacity",
* },
* }
* ```
*/
type PresetType<V extends VariantType> = {
[PresetName: string]: Partial<VariantKV<V>> & { className?: string };
};
/**
* A utility function to provide variants and presets to the component
*
* @param param - Variant Configuration
* @returns function (variantProps) -> class name,
* @returns function (anyProps) -> [variantProps, otherProps]
*/
export function vcn<V extends VariantType>(param: {
/**
* First definition: without presets
*/
base?: string | undefined;
variants: V;
dynamics?: DynamicClassName<V>[];
defaults: VariantKV<V>;
presets?: undefined;
}): [
/**
* Variant Props -> Class Name
*/
(
variantProps: Partial<VariantKV<V>> & {
className?: string;
},
) => string,
/**
* Any Props -> Variant Props, Other Props
*/
// biome-ignore lint/suspicious/noExplicitAny: using unknown causes error `Index signature for type 'string' is missing in type --Props`.
<AnyPropBeforeResolve extends Record<string, any>>(
anyProps: AnyPropBeforeResolve,
) => [
Partial<VariantKV<V>> & {
className?: string;
},
Omit<AnyPropBeforeResolve, keyof Partial<VariantKV<V>> | "className">,
],
];
export function vcn<V extends VariantType, P extends PresetType<V>>(param: {
/**
* Second definition: with presets
*/
base?: string | undefined;
variants: V /* VariantType */;
dynamics?: DynamicClassName<V>[];
defaults: VariantKV<V>;
presets: P;
}): [
/**
* Variant Props -> Class Name
*/
(
variantProps: Partial<VariantKV<V>> & {
className?: string;
preset?: keyof P;
},
) => string,
/**
* Any Props -> Variant Props, Other Props
*/
// biome-ignore lint/suspicious/noExplicitAny: using unknown causes error `Index signature for type 'string' is missing in type --Props`.
<AnyPropBeforeResolve extends Record<string, any>>(
anyProps: AnyPropBeforeResolve,
) => [
Partial<VariantKV<V>> & {
className?: string;
preset?: keyof P;
},
Omit<
AnyPropBeforeResolve,
keyof Partial<VariantKV<V>> | "preset" | "className"
>,
],
];
export function vcn<
V extends VariantType,
P extends PresetType<V> | undefined,
>({
base,
variants,
dynamics = [],
defaults,
presets,
}: {
base?: string | undefined;
variants: V;
dynamics?: DynamicClassName<V>[];
defaults: VariantKV<V>;
presets?: P;
}) {
/**
* --Internal utility function--
* After transforming props to final version (which means "after overriding default, preset, and variant props sent via component props")
* It turns final version of variant props to className
*/
function __transformer__(
final: VariantKV<V>,
dynamics: string[],
propClassName?: string,
): string {
const classNames: string[] = [];
for (const [variantName, variantKey] of Object.entries(
final,
) as VariantKVEntry<V>) {
classNames.push(variants[variantName][variantKey.toString()]);
}
return twMerge(base, ...classNames, ...dynamics, propClassName);
}
return [
/**
* Takes any props (including className), and returns the class name.
* If there is no variant specified in props, then it will fallback to preset, and then default.
*
*
* Process priority of variant will be:
*
* --- Processed as string
* 1. Base
*
* --- Processed as object (it will ignore rest of "not duplicated classname" in lower priority)
* 2. Default
* 3. Preset (overriding default)
* 4. Variant props via component (overriding preset)
*
* --- Processed as string
* 5. Dynamic classNames using variant props
* 6. User's className (overriding dynamic)
*
*
* @param variantProps - The variant props including className.
* @returns The class name.
*/
(
variantProps: { className?: string; preset?: keyof P } & Partial<
VariantKV<V>
>,
) => {
const { className, preset, ..._otherVariantProps } = variantProps;
// Omit<Partial<VariantKV<V>> & { className; preset; }, className | preset> = Partial<VariantKV<V>> (safe to cast)
// We all know `keyof V` = string, right? (but typescript says it's not, so.. attacking typescript with unknown lol)
const otherVariantProps = _otherVariantProps as unknown as Partial<
VariantKV<V>
>;
const kv: VariantKV<V> = { ...defaults };
// Preset Processing
if (presets && preset && preset in presets) {
for (const [variantName, variantKey] of Object.entries(
// typescript bug (casting to NonNullable<P> required)
(presets as NonNullable<P>)[preset],
) as VariantKVEntry<V>) {
kv[variantName] = variantKey;
}
}
// VariantProps Processing
for (const [variantName, variantKey] of Object.entries(
otherVariantProps,
) as VariantKVEntry<V>) {
if (typeof variantKey === "undefined") continue;
kv[variantName] = variantKey;
}
// make dynamics result
const dynamicClasses: string[] = [];
for (const dynamicFunction of dynamics) {
dynamicClasses.push(dynamicFunction(kv));
}
return __transformer__(kv, dynamicClasses, className);
},
/**
* Takes any props, parse variant props and other props.
* If `options.excludeA` is true, then it will parse `A` as "other" props.
* Otherwise, it will parse A as variant props.
*
* @param anyProps - Any props that have passed to the component.
* @returns [variantProps, otherProps]
*/
<AnyPropBeforeResolve extends Record<string, unknown>>(
anyProps: AnyPropBeforeResolve,
) => {
const variantKeys = Object.keys(variants) as (keyof V)[];
return Object.entries(anyProps).reduce(
([variantProps, otherProps], [key, value]) => {
if (
variantKeys.includes(key) ||
key === "className" ||
key === "preset"
) {
return [{ ...variantProps, [key]: value }, otherProps];
}
return [variantProps, { ...otherProps, [key]: value }];
},
[{}, {}],
) as [
Partial<VariantKV<V>> & {
className?: string;
preset?: keyof P;
},
Omit<
typeof anyProps,
keyof Partial<VariantKV<V>> | "preset" | "className"
>,
];
},
];
}
/**
* Extract the props type from return value of `vcn` function.
*
* @example
* ```
* const [variantProps, otherProps] = vcn({ ... })
* interface Props
* extends VariantProps<typeof variantProps>, OtherProps { ... }
*
* function Component(props: Props) {
* ...
* }
* ```
*/
export type VariantProps<F extends (props: Record<string, unknown>) => string> =
F extends (props: infer P) => string ? { [key in keyof P]: P[key] } : never;