diff --git a/package.json b/package.json index 146a409..5a13238 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tailwind-variants", - "version": "0.1.13", + "version": "0.1.14", "description": "🦄 Tailwindcss first-class variant API", "author": "Junior Garcia ", "license": "MIT", @@ -96,7 +96,7 @@ "esm" ] }, - "packageManager": "pnpm@8.3.1", + "packageManager": "pnpm@8.6.8", "engines": { "node": ">=16.x", "pnpm": ">=7.x" diff --git a/src/__tests__/tv.test.ts b/src/__tests__/tv.test.ts index 7138845..564258c 100644 --- a/src/__tests__/tv.test.ts +++ b/src/__tests__/tv.test.ts @@ -691,6 +691,148 @@ describe("Tailwind Variants (TV) - Slots", () => { expectTv(list(), ["list-none", "color--secondary-list", "compound--list"]); expectTv(wrapper(), ["flex", "flex-col", "color--secondary-wrapper", "compound--wrapper"]); }); + + test("should support slot level variant overrides", () => { + const menu = tv({ + base: "text-3xl", + slots: { + title: "text-2xl", + }, + variants: { + color: { + primary: { + base: "color--primary-base", + title: "color--primary-title", + }, + secondary: { + base: "color--secondary-base", + title: "color--secondary-title", + }, + }, + }, + defaultVariants: { + color: "primary", + }, + }); + + const {base, title} = menu(); + + expectTv(base(), ["text-3xl", "color--primary-base"]); + expectTv(title(), ["text-2xl", "color--primary-title"]); + expectTv(base({color: "secondary"}), ["text-3xl", "color--secondary-base"]); + expectTv(title({color: "secondary"}), ["text-2xl", "color--secondary-title"]); + }); + + test("should support slot level variant overrides - compoundSlots", () => { + const menu = tv({ + base: "text-3xl", + slots: { + title: "text-2xl", + subtitle: "text-xl", + }, + variants: { + color: { + primary: { + base: "color--primary-base", + title: "color--primary-title", + subtitle: "color--primary-subtitle", + }, + secondary: { + base: "color--secondary-base", + title: "color--secondary-title", + subtitle: "color--secondary-subtitle", + }, + }, + }, + compoundSlots: [ + { + slots: ["title", "subtitle"], + color: "secondary", + class: ["truncate"], + }, + ], + defaultVariants: { + color: "primary", + }, + }); + + const {base, title, subtitle} = menu(); + + expectTv(base(), ["text-3xl", "color--primary-base"]); + expectTv(title(), ["text-2xl", "color--primary-title"]); + expectTv(subtitle(), ["text-xl", "color--primary-subtitle"]); + expectTv(base({color: "secondary"}), ["text-3xl", "color--secondary-base"]); + expectTv(title({color: "secondary"}), ["text-2xl", "color--secondary-title", "truncate"]); + expectTv(subtitle({color: "secondary"}), ["text-xl", "color--secondary-subtitle", "truncate"]); + }); + + test("should support slot level variant and array variants overrides - compoundSlots", () => { + const menu = tv({ + slots: { + base: "flex flex-wrap", + cursor: ["absolute", "flex", "overflow-visible"], + }, + variants: { + size: { + xs: {}, + sm: {}, + }, + }, + compoundSlots: [ + { + slots: ["base"], + size: ["xs", "sm"], + class: "w-7 h-7 text-xs", + }, + ], + }); + + const {base, cursor} = menu(); + + expect(base()).toEqual("flex flex-wrap w-7 h-7 text-xs"); + expect(base({size: "xs"})).toEqual("flex flex-wrap w-7 h-7 text-xs"); + expect(base({size: "sm"})).toEqual("flex flex-wrap w-7 h-7 text-xs"); + expect(cursor()).toEqual("absolute flex overflow-visible"); + }); + + test("should support slot level variant overrides - compoundVariants", () => { + const menu = tv({ + base: "text-3xl", + slots: { + title: "text-2xl", + }, + variants: { + color: { + primary: { + base: "color--primary-base", + title: "color--primary-title", + }, + secondary: { + base: "color--secondary-base", + title: "color--secondary-title", + }, + }, + }, + compoundVariants: [ + { + color: "secondary", + class: { + title: "truncate", + }, + }, + ], + defaultVariants: { + color: "primary", + }, + }); + + const {base, title} = menu(); + + expectTv(base(), ["text-3xl", "color--primary-base"]); + expectTv(title(), ["text-2xl", "color--primary-title"]); + expectTv(base({color: "secondary"}), ["text-3xl", "color--secondary-base"]); + expectTv(title({color: "secondary"}), ["text-2xl", "color--secondary-title", "truncate"]); + }); }); describe("Tailwind Variants (TV) - Compound Slots", () => { diff --git a/src/index.d.ts b/src/index.d.ts index 134c037..7a39ac9 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -208,8 +208,8 @@ export type TVReturnType< (props?: TVProps): ES extends undefined ? S extends undefined ? string - : {[K in TVSlotsWithBase]: (slotProps?: ClassProp) => string} - : {[K in TVSlotsWithBase]: (slotProps?: ClassProp) => string}; + : {[K in TVSlotsWithBase]: (slotProps?: TVProps) => string} + : {[K in TVSlotsWithBase]: (slotProps?: TVProps) => string}; } & TVReturnProps; export type TV = { diff --git a/src/index.js b/src/index.js index 7719027..61b6748 100644 --- a/src/index.js +++ b/src/index.js @@ -118,38 +118,52 @@ export const tv = (options, configProp) => { let result = acc; if (typeof screenVariantValue === "string") { - result.push( + result = result.concat( removeExtraSpaces(screenVariantValue) .split(" ") .map((v) => `${screen}:${v}`), ); } else if (Array.isArray(screenVariantValue)) { - result.push(screenVariantValue.flatMap((v) => `${screen}:${v}`)); + result = result.concat( + screenVariantValue.reduce((acc, v) => { + return acc.concat(`${screen}:${v}`); + }, []), + ); } else if (typeof screenVariantValue === "object" && typeof slotKey === "string") { - const value = screenVariantValue?.[slotKey]; - - if (value && typeof value === "string") { - const fixedValue = removeExtraSpaces(value); - - result[slotKey] = result[slotKey] - ? [...result[slotKey], ...fixedValue.split(" ").map((v) => `${screen}:${v}`)] - : fixedValue.split(" ").map((v) => `${screen}:${v}`); - } else if (Array.isArray(value) && value.length > 0) { - result[slotKey] = value.flatMap((v) => `${screen}:${v}`); + for (const key in screenVariantValue) { + if (screenVariantValue.hasOwnProperty(key) && key === slotKey) { + const value = screenVariantValue[key]; + + if (value && typeof value === "string") { + const fixedValue = removeExtraSpaces(value); + + if (result[slotKey]) { + result[slotKey] = result[slotKey].concat( + fixedValue.split(" ").map((v) => `${screen}:${v}`), + ); + } else { + result[slotKey] = fixedValue.split(" ").map((v) => `${screen}:${v}`); + } + } else if (Array.isArray(value) && value.length > 0) { + result[slotKey] = value.reduce((acc, v) => { + return acc.concat(`${screen}:${v}`); + }, []); + } + } } } return result; }; - const getVariantValue = (variant, vrs = variants, slotKey = null) => { - const variantObj = vrs?.[variant]; + const getVariantValue = (variant, vrs = variants, slotKey = null, slotProps = null) => { + const variantObj = vrs[variant]; if (!variantObj || isEmptyObject(variantObj)) { return null; } - const variantProp = props?.[variant]; + const variantProp = slotProps?.[variant] ?? props?.[variant]; if (variantProp === null) return null; @@ -164,14 +178,12 @@ export const tv = (options, configProp) => { let screenValues = []; if (typeof variantKey === "object" && responsiveVarsEnabled) { - screenValues = Object.keys(variantKey).reduce((acc, screen) => { - const screenVariantKey = variantKey[screen]; - const screenVariantValue = variantObj?.[screenVariantKey]; + for (const [screen, screenVariantKey] of Object.entries(variantKey)) { + const screenVariantValue = variantObj[screenVariantKey]; if (screen === "initial") { defaultVariantProp = screenVariantKey; - - return acc; + continue; } // if the screen is not in the responsiveVariants array, skip it @@ -179,11 +191,11 @@ export const tv = (options, configProp) => { Array.isArray(config.responsiveVariants) && !config.responsiveVariants.includes(screen) ) { - return acc; + continue; } - return getScreenVariantValues(screen, screenVariantValue, acc, slotKey); - }, []); + screenValues = getScreenVariantValues(screen, screenVariantValue, screenValues, slotKey); + } } const value = variantObj[variantKey] || variantObj[falsyToString(defaultVariantProp)]; @@ -196,7 +208,13 @@ export const tv = (options, configProp) => { return joinObjects(screenValues, value); } - return screenValues.length > 0 ? [value, ...screenValues] : value; + if (screenValues.length > 0) { + screenValues.push(value); + + return screenValues; + } + + return value; }; const getVariantClassNames = () => { @@ -207,13 +225,15 @@ export const tv = (options, configProp) => { return Object.keys(variants).map((vk) => getVariantValue(vk, variants)); }; - const getVariantClassNamesBySlotKey = (slotKey) => { + const getVariantClassNamesBySlotKey = (slotKey, slotProps) => { if (!variants || typeof variants !== "object") { return null; } - return Object.keys(variants).reduce((acc, variant) => { - const variantValue = getVariantValue(variant, variants, slotKey); + const result = new Array(); + + for (const variant in variants) { + const variantValue = getVariantValue(variant, variants, slotKey, slotProps); const value = slotKey === "base" && typeof variantValue === "string" @@ -221,17 +241,22 @@ export const tv = (options, configProp) => { : variantValue && variantValue[slotKey]; if (value) { - acc.push(value); + result[result.length] = value; } + } - return acc; - }, []); + return result; }; - const propsWithoutUndefined = - props && Object.fromEntries(Object.entries(props).filter(([, value]) => value !== undefined)); + const propsWithoutUndefined = {}; + + for (const prop in props) { + if (props[prop] !== undefined) { + propsWithoutUndefined[prop] = props[prop]; + } + } - const getCompleteProps = (key) => { + const getCompleteProps = (key, slotProps) => { const initialProp = typeof props?.[key] === "object" ? { @@ -243,113 +268,130 @@ export const tv = (options, configProp) => { ...defaultVariants, ...propsWithoutUndefined, ...initialProp, + ...slotProps, }; }; - const getCompoundVariantsValue = (cv = []) => - cv - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ?.filter(({class: tvClass, className: tvClassName, ...compoundVariantOptions}) => - Object.entries(compoundVariantOptions).every(([key, value]) => { - const completeProps = getCompleteProps(key); + const getCompoundVariantsValue = (cv = [], slotProps) => { + const result = []; + + for (const {class: tvClass, className: tvClassName, ...compoundVariantOptions} of cv) { + let isValid = true; + + for (const [key, value] of Object.entries(compoundVariantOptions)) { + const completeProps = getCompleteProps(key, slotProps); + + if (Array.isArray(value)) { + if (!value.includes(completeProps[key])) { + isValid = false; + break; + } + } else { + if (completeProps[key] !== value) { + isValid = false; + break; + } + } + } + + if (isValid) { + tvClass && result.push(tvClass); + tvClassName && result.push(tvClassName); + } + } - return Array.isArray(value) - ? value.includes(completeProps[key]) - : completeProps[key] === value; - }), - ) - .flatMap(({class: tvClass, className: tvClassName}) => [tvClass, tvClassName]); + return result; + }; - const getCompoundVariantClassNames = () => { - const cvValues = getCompoundVariantsValue(compoundVariants); - const ecvValues = getCompoundVariantsValue(extend?.compoundVariants); + const getCompoundVariantClassNames = (slotProps) => { + const cvValues = getCompoundVariantsValue(compoundVariants, slotProps); + const ecvValues = getCompoundVariantsValue(extend?.compoundVariants, slotProps); return flatMergeArrays(ecvValues, cvValues); }; - const getCompoundVariantClassNamesBySlot = () => { - const compoundClassNames = getCompoundVariantClassNames(compoundVariants); + const getCompoundVariantClassNamesBySlot = (slotProps) => { + const compoundClassNames = getCompoundVariantClassNames(slotProps); if (!Array.isArray(compoundClassNames)) { return compoundClassNames; } - return compoundClassNames.reduce((acc, className) => { + const result = {}; + + for (const className of compoundClassNames) { if (typeof className === "string") { - acc.base = cn(acc.base, className)(config); + result.base = cn(result.base, className)(config); } if (typeof className === "object") { - const classNameKeys = Object.keys(className); - - for (const slot of classNameKeys) { - acc[slot] = cn(acc[slot], className[slot])(config); + for (const [slot, slotClassName] of Object.entries(className)) { + result[slot] = cn(result[slot], slotClassName)(config); } } + } - return acc; - }, {}); + return result; }; - const getCompoundSlotClassNameBySlot = () => { + const getCompoundSlotClassNameBySlot = (slotProps) => { if (compoundSlots.length < 1) { return null; } - return compoundSlots.reduce((acc, slot) => { - const {slots = [], class: slotClass, className: slotClassName, ...slotVariants} = slot; + const result = {}; + + for (const { + slots = [], + class: slotClass, + className: slotClassName, + ...slotVariants + } of compoundSlots) { + for (const slotName of slots) { + result[slotName] = result[slotName] || []; + result[slotName].push([slotClass, slotClassName]); + } if (!isEmptyObject(slotVariants)) { - const slotVariantsKeys = Object.keys(slotVariants); + let isValid = true; - for (const key of slotVariantsKeys) { - const completePropsValue = getCompleteProps(key)[key]; + for (const key of Object.keys(slotVariants)) { + const completePropsValue = getCompleteProps(key, slotProps)[key]; - // if none of the slot variant keys are included in props or default variants then skip the slot - // if the included slot variant key is not equal to the slot variant value then skip the slot if (completePropsValue === undefined || completePropsValue !== slotVariants[key]) { - return acc; + isValid = false; + break; } } - } - for (const slotName of slots) { - if (!acc[slotName]) { - acc[slotName] = []; + if (!isValid) { + continue; } - - acc[slotName].push([slotClass, slotClassName]); } + } - return acc; - }, {}); + return result; }; // with slots if (!isEmptyObject(slotProps) || !isEmptyObject(extend?.slots)) { - const compoundClassNames = getCompoundVariantClassNamesBySlot() ?? []; - const compoundSlotClassNames = getCompoundSlotClassNameBySlot() ?? []; - - const slotsFns = - typeof slots === "object" && !isEmptyObject(slots) - ? Object.keys(slots).reduce((acc, slotKey) => { - acc[slotKey] = (slotProps) => - cn( - slots[slotKey], - getVariantClassNamesBySlotKey(slotKey), - compoundClassNames?.[slotKey], - compoundSlotClassNames?.[slotKey], - slotProps?.class, - slotProps?.className, - )(config); - - return acc; - }, {}) - : {}; + const slotsFns = {}; + + if (typeof slots === "object" && !isEmptyObject(slots)) { + for (const slotKey of Object.keys(slots)) { + slotsFns[slotKey] = (slotProps) => + cn( + slots[slotKey], + getVariantClassNamesBySlotKey(slotKey, slotProps), + (getCompoundVariantClassNamesBySlot(slotProps) ?? [])[slotKey], + (getCompoundSlotClassNameBySlot(slotProps) ?? [])[slotKey], + slotProps?.class, + slotProps?.className, + )(config); + } + } - return { - ...slotsFns, - }; + return slotsFns; } // normal variants diff --git a/src/utils.js b/src/utils.js index 0356394..6eb016f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -24,21 +24,27 @@ export function flatArray(arr) { export const flatMergeArrays = (...arrays) => flatArray(arrays).filter(Boolean); export const mergeObjects = (obj1, obj2) => { - let result = {}; - - for (let key in obj1) { - if (obj2?.hasOwnProperty(key)) { - result[key] = - typeof obj1[key] === "object" - ? mergeObjects(obj1[key], obj2[key]) - : obj2[key] + " " + obj1[key]; + const result = {}; + const keys1 = Object.keys(obj1); + const keys2 = Object.keys(obj2); + + for (const key of keys1) { + if (keys2.includes(key)) { + const val1 = obj1[key]; + const val2 = obj2[key]; + + if (typeof val1 === "object" && typeof val2 === "object") { + result[key] = mergeObjects(val1, val2); + } else { + result[key] = val2 + " " + val1; + } } else { result[key] = obj1[key]; } } - for (let key in obj2) { - if (!result.hasOwnProperty(key)) { + for (const key of keys2) { + if (!keys1.includes(key)) { result[key] = obj2[key]; } }