Skip to content
Open
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"devDependencies": {
"@changesets/cli": "^2.26.0",
"tailwindcss": "^3.2.4",
"ts-essentials": "^9.4.1",
"typescript": "^4.9.5"
},
"dependencies": {
Expand All @@ -34,4 +35,4 @@
"tailwindcss": "^3.x.x",
"colord": "^2.x.x"
}
}
}
5 changes: 3 additions & 2 deletions src/ColorProperty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
120 changes: 44 additions & 76 deletions src/Theme.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<T extends ThemeProps = ThemeProps> {
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;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    if (options?.selector) this.selector = options.selector;

Please revert this syntax or add curly braces.


if (options?.themePropertiesConfig) {
this.themePropertiesConfig = {
...defaultThemePropertiesConfig,
...options.themePropertiesConfig,
};
}

const { cssVariables, cssProperties } = this.getCSS(theme, this.userPrefix);

Expand All @@ -103,24 +68,27 @@ export class Theme<T extends ThemeProps = ThemeProps> {
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,
Expand All @@ -136,10 +104,10 @@ export class Theme<T extends ThemeProps = ThemeProps> {
};
}

variant(theme: PartialThemePropertyConfig<T>, options?: VariantOptions) {
variant(theme: DeepPartial<ThemePropertyConfig>, 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] = {
Expand Down
29 changes: 15 additions & 14 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -19,7 +18,9 @@ export type VariantOptions = {
};

/** Tailwind config's "theme" type: */
export type ThemeProps = Partial<ThemeConfig>;
export type ThemeProps = Partial<
Omit<ThemeConfig, "screens" | "supports" | "data">
>;

/**
* ${prefix}${key}: <hsl-values>
Expand All @@ -35,14 +36,9 @@ export type CssVariables = Record<string, string>;
======================= */

export type FlatThemePropertyConfig = Record<string, string>;
export type ThemePropertyConfig = Record<
string,
FlatThemePropertyConfig | string
>;
export type PartialThemePropertyConfig<
PrimaryTheme extends ThemePropertyConfig
> = {
[K in keyof PrimaryTheme]?: Partial<PrimaryTheme[K]>;

export type ThemePropertyConfig = {
[key: string]: string | ThemePropertyConfig;
};

export type ThemePropertyOptions = {
Expand All @@ -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;
};
Expand Down