responsive-class-variants
Version:
rcv helps you create responsive class variants
198 lines • 7.22 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) => isSingularValue(value)
? mapper(value)
: Object.fromEntries(Object.entries(value).map(([breakpoint, value]) => [
breakpoint,
mapper(value),
]));
// Helper functions for slots
const isSlotsConfig = (config) => {
return "slots" in config;
};
const prefixClasses = (classes, prefix) => classes
.split(" ")
.map((className) => `${prefix}:${className}`)
.join(" ");
// Helper function to get variant value for a specific slot or base
const getVariantValue = (variants, key, value, slotName) => {
const variant = variants?.[key];
const variantValue = variant?.[value];
// Handle string values
if (typeof variantValue === "string") {
return variantValue;
}
// Handle object values (slot-specific classes)
if (typeof variantValue === "object" &&
variantValue !== null &&
slotName &&
slotName in variantValue) {
const slotSpecificValue = variantValue[slotName];
if (typeof slotSpecificValue === "string") {
return slotSpecificValue;
}
}
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);
})
.filter(Boolean)
.join(" ");
};
// 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);
})
.filter(Boolean)
.join(" ");
};
// Helper function to match compound variants
const matchesCompoundVariant = (compound, props) => {
return Object.entries(compound).every(([key, value]) => props[key] === String(value) ||
props[key] === value);
};
const createSlotFunction = (slotConfig, variants, compoundVariants, onComplete, slotName) => ({ className, ...props } = {}) => {
const responsiveClasses = processVariantProps(props, variants, slotName);
const compoundClasses = compoundVariants
?.map(({ class: slotClasses, className: compoundClassName, ...compound }) => {
if (matchesCompoundVariant(compound, props)) {
// If compound variant has slot-specific classes, use those for this slot
if (slotClasses &&
typeof slotClasses === "object" &&
slotClasses[slotName]) {
return slotClasses[slotName];
}
// Otherwise use the general className
return compoundClassName;
}
return undefined;
})
.filter(Boolean);
const classes = clsx(slotConfig, responsiveClasses, compoundClasses, className);
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;
const slotFunctions = {};
// Create slot functions for each slot
for (const [slotName, slotConfig] of Object.entries(slots)) {
slotFunctions[slotName] = createSlotFunction(slotConfig, variants, compoundVariants, onComplete, slotName);
}
return slotFunctions;
}
// If config is not a slots config, create a base function
const { base, variants, compoundVariants, onComplete } = config;
return ({ className, ...props }) => {
const responsiveClasses = processVariantProps(props, variants);
const compoundClasses = compoundVariants
?.map(({ className: compoundClassName, ...compound }) => {
if (matchesCompoundVariant(compound, props)) {
return compoundClassName;
}
return undefined;
})
.filter(Boolean);
const classes = clsx(base, responsiveClasses, compoundClasses, className);
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