diff --git a/README.md b/README.md index 9c9a320..22f9507 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ const button = windCtrl({ // Usage button({ w: "w-full" }); // -> className includes "w-full" (static utility) -button({ w: 200 }); // -> style includes { width: "200px" } (dynamic value) +button({ w: 200 }); // -> style includes { width: "200px" } (dynamic value) ``` > **Note on Tailwind JIT**: Tailwind only generates CSS for class names it can statically detect in your source. Avoid constructing class strings dynamically (e.g. "`w-`" + `size`) unless you safelist them in your Tailwind config. @@ -187,6 +187,7 @@ button({ intent: "primary", size: "lg" }); - **Tailwind JIT:** Tailwind only generates CSS for class names it can statically detect. Avoid constructing class strings dynamically unless you safelist them. - **Traits precedence:** If trait order matters, use the array form (`traits: ["a", "b"]`) to make precedence explicit. - **SSR/RSC:** Keep dynamic resolvers pure (same input → same output) to avoid hydration mismatches. +- **Static config:** `windCtrl` configuration is treated as static/immutable. Mutating the config object after creation is not supported. ## License diff --git a/src/__tests__/windctrl.test.ts b/src/index.test.ts similarity index 99% rename from src/__tests__/windctrl.test.ts rename to src/index.test.ts index cf1c005..61708c6 100644 --- a/src/__tests__/windctrl.test.ts +++ b/src/index.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { windCtrl } from "../index"; +import { windCtrl } from "./"; describe("windCtrl", () => { describe("Base classes", () => { diff --git a/src/index.ts b/src/index.ts index b02289c..1ccbb7d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -75,15 +75,15 @@ function processTraits>( return []; } -function processDynamic>( - dynamic: TDynamic, - props: Props<{}, {}, TDynamic>, +function processDynamicEntries( + entries: [string, DynamicResolver][], + props: Record, ): { className: ClassValue[]; style: CSSProperties } { const classNameParts: ClassValue[] = []; const styles: CSSProperties[] = []; - for (const [key, resolver] of Object.entries(dynamic)) { - const value = props[key as keyof TDynamic]; + for (const [key, resolver] of entries) { + const value = props[key]; if (value !== undefined && value !== null) { const result = resolver(value); if (typeof result === "string") { @@ -137,6 +137,16 @@ export function windCtrl< defaultVariants = {}, } = config; + const resolvedVariants = Object.entries(variants) as [ + string, + Record, + ][]; + const resolvedDynamicEntries = Object.entries(dynamic) as [ + string, + DynamicResolver, + ][]; + const resolvedScopeClasses = processScopes(scopes); + return (props = {} as Props) => { const classNameParts: ClassValue[] = []; let mergedStyle: CSSProperties = {}; @@ -150,34 +160,33 @@ export function windCtrl< } // 2. Variants (with defaultVariants fallback) - if (variants) { - for (const [variantKey, variantOptions] of Object.entries(variants)) { - const propValue = - props[variantKey as keyof typeof props] ?? - defaultVariants[variantKey as keyof typeof defaultVariants]; - if (propValue && variantOptions[propValue as string]) { - classNameParts.push(variantOptions[propValue as string]); - } + for (const [variantKey, variantOptions] of resolvedVariants) { + const propValue = + props[variantKey as keyof typeof props] ?? + defaultVariants[variantKey as keyof typeof defaultVariants]; + if (propValue && variantOptions[propValue as string]) { + classNameParts.push(variantOptions[propValue as string]); } } // 3. Traits (higher priority than variants) - if (traits && props.traits) { - const traitClasses = processTraits(traits, props.traits); - classNameParts.push(...traitClasses); + if (props.traits) { + classNameParts.push(...processTraits(traits, props.traits)); } // 4. Dynamic (highest priority for className) - if (dynamic) { - const dynamicResult = processDynamic(dynamic, props); + if (resolvedDynamicEntries.length) { + const dynamicResult = processDynamicEntries( + resolvedDynamicEntries, + props, + ); classNameParts.push(...dynamicResult.className); mergedStyle = mergeStyles(mergedStyle, dynamicResult.style); } // 5. Scopes (always applied, but don't conflict with other classes) - if (scopes) { - const scopeClasses = processScopes(scopes); - classNameParts.push(...scopeClasses); + if (resolvedScopeClasses.length) { + classNameParts.push(...resolvedScopeClasses); } const finalClassName = twMerge(clsx(classNameParts));