responsive-class-variants
Version:
rcv helps you create responsive class variants
208 lines • 7.82 kB
JavaScript
import { clsx } from "clsx";
const isSingularValue = (value) => !isBreakpointsMap(value);
const isBreakpointsMap = (value) => typeof value === "object" && value != null && !Array.isArray(value);
/**
* Maps a ResponsiveValue to a new ResponsiveValue using the provided mapper function. Singular values are passed through as is.
*
* @template V The type of the original value
* @template T The type of the mapped value
* @template B The type of breakpoints
* @param {ResponsiveValue<V, B>} value - The original ResponsiveValue to be mapped
* @param {function(V): T} mapper - A function that maps a ResponsiveValue to a new ResponsiveValue
* @returns {ResponsiveValue<T, B>} A new ResponsiveValue with the mapped values
*
*
* @example
* const sizes = {
* initial: 'md',
* sm: 'lg',
* }
*
* const output = mapResponsiveValue(sizes, size => {
* switch (size) {
* case 'initial':
* return 'sm';
* case 'sm':
* return 'md';
* }
* });
*
* // console.log(output)
* {
* initial: 'sm',
* sm: 'md',
* }
*/
export const mapResponsiveValue = (value, mapper) => {
if (isSingularValue(value)) {
return mapper(value);
}
const result = {};
for (const key of Object.keys(value)) {
result[key] = mapper(value[key]);
}
return result;
};
// Helper functions for slots
const isSlotsConfig = (config) => {
return "slots" in config;
};
const normalizeClassValue = (value) => {
if (Array.isArray(value)) {
return value.join(" ");
}
if (typeof value === "string") {
return value;
}
return undefined;
};
const prefixClasses = (classes, prefix) => classes.replace(/(\S+)/g, `${prefix}:$1`);
// Helper function to get variant value for a specific slot or base
const getVariantValue = (variants, key, value, slotName) => {
const variantValue = variants?.[key]?.[value];
// Early return if no variant value found
if (variantValue == null)
return undefined;
// Handle string or array values directly
if (typeof variantValue === "string" || Array.isArray(variantValue)) {
return normalizeClassValue(variantValue);
}
// Handle slot-specific values (object with slot keys)
if (slotName && slotName in variantValue) {
return normalizeClassValue(variantValue[slotName]);
}
return undefined;
};
// Helper function to process responsive values
const processResponsiveValue = (variants, key, value, slotName) => {
return Object.entries(value).map(([breakpoint, breakpointValue]) => {
const variantValue = getVariantValue(variants, key, breakpointValue, slotName);
if (!variantValue)
return undefined;
// If the breakpoint is initial, return without prefix
if (breakpoint === "initial") {
return variantValue;
}
// Otherwise, return with breakpoint prefix
return prefixClasses(variantValue, breakpoint);
});
};
// Helper function to process variant props into classes
const processVariantProps = (props, variants, slotName) => {
return Object.entries(props).map(([key, propValue]) => {
const value = typeof propValue === "boolean" ? String(propValue) : propValue;
// Handle undefined values
if (!value)
return undefined;
// Handle singular values
if (typeof value === "string") {
return getVariantValue(variants, key, value, slotName);
}
// Handle responsive values
return processResponsiveValue(variants, key, value, slotName);
});
};
// Helper function to match compound variants
const matchesCompoundVariant = (compound, props) => {
return Object.entries(compound).every(([key, value]) => {
const propValue = props[key];
// Direct comparison first, then try string conversion for boolean handling
return propValue === value || propValue === String(value);
});
};
// Helper function to extract class value from compound variant class prop
const getCompoundVariantSlotClass = (classValue, slotName) => {
if (!classValue)
return undefined;
if (typeof classValue === "object" && classValue[slotName]) {
return normalizeClassValue(classValue[slotName]);
}
if (typeof classValue === "string") {
return classValue;
}
return undefined;
};
const createSlotFunction = (slotConfig, variants, compoundVariants, onComplete, slotName) => ({ className, class: classFromProps, ...props } = {}) => {
const responsiveClasses = processVariantProps(props, variants, slotName);
const compoundClasses = compoundVariants?.map(({ class: classFromCompound, className: classNameFromCompound, ...compound }) => {
if (matchesCompoundVariant(compound, props)) {
return [
getCompoundVariantSlotClass(classFromCompound, slotName),
getCompoundVariantSlotClass(classNameFromCompound, slotName),
];
}
return undefined;
});
const classes = clsx(slotConfig, responsiveClasses, compoundClasses, className, classFromProps);
return onComplete ? onComplete(classes) : classes;
};
export function rcv(config) {
// Check if config is a slots config
if (isSlotsConfig(config)) {
const { slots, variants, compoundVariants, onComplete } = config;
return () => {
const slotFunctions = {};
// Create slot functions for each slot - ensure all slots are always present
for (const [slotName, slotConfig] of Object.entries(slots)) {
const slotFunction = createSlotFunction(slotConfig, variants, compoundVariants, onComplete, slotName);
slotFunctions[slotName] = slotFunction;
}
return slotFunctions;
};
}
// If config is not a slots config, create a base function
const { base, variants, compoundVariants, onComplete } = config;
return ({ className, class: classFromProps, ...props } = {}) => {
const responsiveClasses = processVariantProps(props, variants);
const compoundClasses = compoundVariants?.map(({ className: compoundClassName, ...compound }) => {
if (matchesCompoundVariant(compound, props)) {
return compoundClassName;
}
return undefined;
});
const classes = clsx(base, responsiveClasses, compoundClasses, className, classFromProps);
return onComplete ? onComplete(classes) : classes;
};
}
/**
* Creates a custom rcv function with custom breakpoints and an optional onComplete callback
*
* @template B - The custom breakpoints type
* @param breakpoints - Optional array of custom breakpoint names
* @param onComplete - Optional callback function that receives the generated classes and returns the final classes
* @returns A function that creates rcv with custom breakpoints
*
* @example
* const customRcv = createRcv(['mobile', 'tablet', 'desktop']);
*
* const getButtonVariants = customRcv({
* base: "px-4 py-2 rounded",
* variants: {
* intent: {
* primary: "bg-blue-500 text-white",
* secondary: "bg-gray-200 text-gray-800"
* }
* }
* });
*
* // Usage with custom breakpoints:
* getButtonVariants({ intent: { initial: "primary", mobile: "secondary", desktop: "primary" } })
*/
export const createRcv = (_breakpoints, onComplete) => {
function customRcv(config) {
if (isSlotsConfig(config)) {
return rcv({
...config,
onComplete: onComplete || config.onComplete,
});
}
else {
return rcv({
...config,
onComplete: onComplete || config.onComplete,
});
}
}
return customRcv;
};
//# sourceMappingURL=index.js.map