diff --git a/package.json b/package.json index aaaaa1c..47afb1f 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "devDependencies": { "@changesets/cli": "^2.26.0", "tailwindcss": "^3.2.4", + "ts-essentials": "^9.4.1", "typescript": "^4.9.5" }, "dependencies": { @@ -34,4 +35,4 @@ "tailwindcss": "^3.x.x", "colord": "^2.x.x" } -} +} \ No newline at end of file diff --git a/src/ColorProperty.ts b/src/ColorProperty.ts index 9da24fc..7a860d5 100644 --- a/src/ColorProperty.ts +++ b/src/ColorProperty.ts @@ -3,8 +3,9 @@ import { ThemeProperty } from "./ThemeProperty"; import { ThemeValueFilterProps } from "./types"; /** - * ColorProperty extends the base `ThemeProperty` class in order to transform - * the output of CSS variable prefixes/values and CSS property values. + * ColorProperty extends the base `ThemeProperty` class in order to customize how + * Tailwind theme color property values get output to CSS variables; it converts + * user-defined HEX codes to HSL values, with fall-back to the HEX codes. */ export class ColorProperty extends ThemeProperty { // inject "color" into the generated CSS variable names: diff --git a/src/Theme.ts b/src/Theme.ts index fabf571..d2165a8 100644 --- a/src/Theme.ts +++ b/src/Theme.ts @@ -1,11 +1,12 @@ import type { CssVariables, - InternalThemePropertiesConfig, - PartialThemePropertyConfig, + ThemePropertiesConfig, ThemeOptions, + ThemePropertyConfig, ThemeProps, VariantOptions, } from "./types"; +import type { DeepPartial } from "ts-essentials"; import type { CSSRuleObject } from "tailwindcss/types/config"; import { withOptions } from "tailwindcss/plugin"; import { ColorProperty } from "./ColorProperty"; @@ -17,81 +18,45 @@ import { camelToKebab } from "./utils/camelToKebab"; * 1. Customize the property-prefix that gets injected into the generated CSS Variable name * Note: this is optional; if you don't specify a prefix, it'll get autogenerated by converting the property name to kebab-case * 2. Attach properties to `ThemeProperty` child classes, enabling custom value conversion logic - * - * TODO: consider exposing a user-facing API for configuring this themePropertiesConfig; users could create their own ThemeProperty sub-classes (like ColorProperty) to write their own value conversion logic.. would make tailwind-easy-theme incredibly flexible -- like more of a low-level theming framework */ -const themePropertiesConfig: InternalThemePropertiesConfig = { - colors: { - prefix: "", - type: ColorProperty, - }, - backgroundColor: { - prefix: "bg", - type: ColorProperty, - }, - textColor: { - prefix: "text", - type: ColorProperty, - }, - borderColor: { - prefix: "border", - type: ColorProperty, - }, - accentColor: { - prefix: "accent", - type: ColorProperty, - }, - ringColor: { - prefix: "ring", - type: ColorProperty, - }, - caretColor: { - prefix: "caret", - type: ColorProperty, - }, - divideColor: { - prefix: "divide", - type: ColorProperty, - }, - outlineColor: { - prefix: "outline", - type: ColorProperty, - }, - boxShadowColor: { - prefix: "box-shadow", - type: ColorProperty, - }, - ringOffsetColor: { - prefix: "ring-offset", - type: ColorProperty, - }, - placeholderColor: { - prefix: "placeholder", - type: ColorProperty, - }, - textDecorationColor: { - prefix: "text-decoration", - type: ColorProperty, - }, - gradientColorStops: { - prefix: "gradient", - type: ColorProperty, - }, - fill: { - prefix: "fill", - type: ColorProperty, - }, +const defaultThemePropertiesConfig: ThemePropertiesConfig = { + colors: { prefix: "", type: ColorProperty }, + backgroundColor: { prefix: "bg", type: ColorProperty }, + textColor: { prefix: "text", type: ColorProperty }, + borderColor: { prefix: "border", type: ColorProperty }, + accentColor: { prefix: "accent", type: ColorProperty }, + ringColor: { prefix: "ring", type: ColorProperty }, + caretColor: { prefix: "caret", type: ColorProperty }, + divideColor: { prefix: "divide", type: ColorProperty }, + outlineColor: { prefix: "outline", type: ColorProperty }, + boxShadowColor: { prefix: "box-shadow", type: ColorProperty }, + ringOffsetColor: { prefix: "ring-offset", type: ColorProperty }, + placeholderColor: { prefix: "placeholder", type: ColorProperty }, + textDecorationColor: { prefix: "text-decoration", type: ColorProperty }, + gradientColorStops: { prefix: "gradient", type: ColorProperty }, + fill: { prefix: "fill", type: ColorProperty }, + stroke: { prefix: "stroke", type: ColorProperty }, }; export class Theme { private userPrefix: string | undefined; private selector: string = ":root"; + private themePropertiesConfig: ThemePropertiesConfig = + defaultThemePropertiesConfig; private cssProperties: T = {} as T; private cssRules: CSSRuleObject = {}; constructor(theme: T, options?: ThemeOptions) { this.userPrefix = options?.prefix; - this.selector = options?.selector || this.selector; + + if (options?.selector) this.selector = options.selector; + + if (options?.themePropertiesConfig) { + this.themePropertiesConfig = { + ...defaultThemePropertiesConfig, + ...options.themePropertiesConfig, + }; + } const { cssVariables, cssProperties } = this.getCSS(theme, this.userPrefix); @@ -103,24 +68,27 @@ export class Theme { let allCssVariables: CssVariables = {}; let allCssProperties: T = {} as T; - Object.keys(theme).forEach((propertyKey) => { + (Object.keys(theme) as (keyof ThemeProps)[]).forEach((propertyKey) => { const propertyValue = theme[propertyKey]; if (!propertyValue) return; let prefix: string = camelToKebab(propertyKey); let Property = ThemeProperty; - if (propertyKey in themePropertiesConfig) { - const config = themePropertiesConfig[propertyKey]; - prefix = config.prefix ?? prefix; - Property = config.type ?? Property; + if (propertyKey in this.themePropertiesConfig) { + const config = this.themePropertiesConfig[propertyKey]; + prefix = config?.prefix ?? prefix; + Property = config?.type ?? Property; } prefix = userPrefix ? `${userPrefix}-${prefix}` : prefix; - const { cssVariables, cssProperties } = new Property(propertyValue, { - prefix, - }).getCSS(); + const { cssVariables, cssProperties } = new Property( + propertyValue as ThemePropertyConfig, + { + prefix, + } + ).getCSS(); allCssVariables = { ...allCssVariables, @@ -136,10 +104,10 @@ export class Theme { }; } - variant(theme: PartialThemePropertyConfig, options?: VariantOptions) { + variant(theme: DeepPartial, options?: VariantOptions) { const mediaQuery = options?.mediaQuery; - const { cssVariables } = this.getCSS(theme as T, this.userPrefix); + const { cssVariables } = this.getCSS(theme, this.userPrefix); if (mediaQuery) { this.cssRules[mediaQuery] = { diff --git a/src/types.ts b/src/types.ts index d4586f3..cc67e92 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,14 +1,13 @@ import { ThemeConfig } from "tailwindcss/types/config"; import { ThemeProperty } from "./ThemeProperty"; -// export type TailwindThemeColorProperty = -// (typeof colorProperties)[number]; - export type ThemeOptions = { /** The prefix added to the key of a color. Defaults to `--color-` */ prefix?: string; /** The selector to add the css variables to. Defaults to `:root` */ selector?: string; + /** Customize how theme properties get converted to CSS variables */ + themePropertiesConfig?: ThemePropertiesConfig; }; export type VariantOptions = { @@ -19,7 +18,9 @@ export type VariantOptions = { }; /** Tailwind config's "theme" type: */ -export type ThemeProps = Partial; +export type ThemeProps = Partial< + Omit +>; /** * ${prefix}${key}: @@ -35,14 +36,9 @@ export type CssVariables = Record; ======================= */ export type FlatThemePropertyConfig = Record; -export type ThemePropertyConfig = Record< - string, - FlatThemePropertyConfig | string ->; -export type PartialThemePropertyConfig< - PrimaryTheme extends ThemePropertyConfig -> = { - [K in keyof PrimaryTheme]?: Partial; + +export type ThemePropertyConfig = { + [key: string]: string | ThemePropertyConfig; }; export type ThemePropertyOptions = { @@ -52,8 +48,13 @@ export type ThemePropertyOptions = { // Constructor signature for classes extending ThemeProperty export type ThemePropertyConstructor = new (...args: any[]) => ThemeProperty; -export type InternalThemePropertiesConfig = { - [P: keyof ThemeConfig]: { +export type ThemePropertiesConfig = { + [P in keyof ThemeProps]: { + prefix: string; + type: ThemePropertyConstructor; + }; +} & { + [key: string]: { prefix: string; type: ThemePropertyConstructor; }; diff --git a/src/utils/flattenThemeConfig.ts b/src/utils/flattenThemeConfig.ts index c44bd83..f765aa9 100644 --- a/src/utils/flattenThemeConfig.ts +++ b/src/utils/flattenThemeConfig.ts @@ -18,7 +18,7 @@ export function flattenThemeConfig( const { DEFAULT, ...rest } = value; if (DEFAULT) { - flattenedThemeConfig[key] = DEFAULT; + flattenedThemeConfig[key] = DEFAULT as string; } const nestedThemeProperty = flattenThemeConfig(rest, key);