vueless
Version:
Vue Styleless UI Component Library, powered by Tailwind CSS.
146 lines (124 loc) • 4.05 kB
text/typescript
import { merge } from "lodash-es";
import { defineConfig } from "cva";
import { extendTailwindMerge } from "tailwind-merge";
import { isCSR, isSSR } from "./helper.ts";
import { createGetMergedConfig } from "./node/mergeConfigs.js";
import { COMPONENT_NAME as U_ICON } from "../ui.image-icon/constants.ts";
import { getSelectedBrandColor } from "./theme.ts";
import {
BRAND_COLOR,
GRAYSCALE_COLOR,
ICON_NON_PROPS_DEFAULTS,
TAILWIND_MERGE_EXTENSION,
} from "../constants.js";
import type {
Config,
Defaults,
Strategies,
BrandColors,
UnknownObject,
ComponentNames,
} from "../types.ts";
interface MergedConfigOptions {
defaultConfig: unknown;
globalConfig: unknown;
propsConfig?: unknown;
vuelessStrategy?: Strategies;
}
type GetMergedConfig = (options: MergedConfigOptions) => unknown;
/**
* Load Vueless config from the project root.
* Both for server and client side renderings.
* IIFE for SSR is used to prevent top level await issue.
*/
export let vuelessConfig: Config = {};
if (isSSR) {
/* Load Vueless config from the project root in IIFE (no top-level await). */
(async () => {
try {
const filePath = `${process.cwd()}/vueless.config`;
vuelessConfig = (await import(/* @vite-ignore */ `${filePath}.js`)).default;
if (!vuelessConfig) {
vuelessConfig = (await import(/* @vite-ignore */ `${filePath}.ts`)).default;
}
} catch {
vuelessConfig = {};
}
})();
}
if (isCSR) {
vuelessConfig =
Object.values(
import.meta.glob(["/vueless.config.{js,ts}", "/**/vueless.config.{js,ts}"], {
eager: true,
import: "default",
}),
)[0] || {};
}
/**
* Extend twMerge (tailwind merge) by vueless and user config:
*/
const twMerge = extendTailwindMerge(merge(TAILWIND_MERGE_EXTENSION, vuelessConfig.tailwindMerge));
/**
* Export cva (class variance authority) methods:
* – extended with tailwind-merge
* – remove all Vueless nested component names ({U...} strings) from class list string.
* Learn more here: https://beta.cva.style
*/
export const {
cx,
compose,
cva: classVarianceAuthority,
} = defineConfig({
hooks: {
onComplete: (classNames) => twMerge(classNames),
},
});
export const getMergedConfig = createGetMergedConfig(cx) as GetMergedConfig;
/* This allows skipping some CVA config keys in vueless config. */
export const cva = ({ base = "", variants = {}, compoundVariants = [], defaultVariants = {} }) =>
classVarianceAuthority({
base,
variants,
compoundVariants,
defaultVariants,
});
/**
* Return default values for component props, icons, etc..
*/
export function getDefaults<Props, Config>(defaultConfig: Config, name: ComponentNames) {
const componentDefaults = (defaultConfig as UnknownObject).defaults || {};
const globalDefaults = vuelessConfig.components?.[name]?.defaults || {};
const defaults = merge({}, componentDefaults, globalDefaults) as Props & Defaults;
if (defaults.color) {
defaults.color = getColor(defaults.color as BrandColors);
}
/* Remove non a props defaults. */
for (const key in defaults) {
const isNonPropIcon = /Icon/.test(key) && !/(leftIcon|rightIcon)/.test(key);
const isNonPropIconDefaults = ICON_NON_PROPS_DEFAULTS.includes(key) && name === U_ICON;
if (isNonPropIcon || isNonPropIconDefaults) {
delete defaults[key];
}
}
return {
...defaults,
dataTest: "",
config: () => ({}),
};
}
/**
* Return `grayscale` color if in component config it `brand` but in vueless config it `grayscale`
* Otherwise return given color.
*/
export function getColor(color: string) {
const isComponentColorBrand = color === BRAND_COLOR;
const isSelectedColorGrayscale = getSelectedBrandColor() === GRAYSCALE_COLOR;
return isComponentColorBrand && isSelectedColorGrayscale ? GRAYSCALE_COLOR : color;
}
/**
* Replace in tailwind classes `{color}` variable into given color.
*/
export function setColor(classes: string, color: string) {
return classes?.replace(/{color}/g, color);
}