Skip to content

Commit fdb5014

Browse files
authored
Merge pull request #1 from morishxt/pre-compute
Precompute operations that don't depend on `props`
2 parents 806458e + e051563 commit fdb5014

File tree

3 files changed

+33
-23
lines changed

3 files changed

+33
-23
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ const button = windCtrl({
103103

104104
// Usage
105105
button({ w: "w-full" }); // -> className includes "w-full" (static utility)
106-
button({ w: 200 }); // -> style includes { width: "200px" } (dynamic value)
106+
button({ w: 200 }); // -> style includes { width: "200px" } (dynamic value)
107107
```
108108

109109
> **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" });
187187
- **Tailwind JIT:** Tailwind only generates CSS for class names it can statically detect. Avoid constructing class strings dynamically unless you safelist them.
188188
- **Traits precedence:** If trait order matters, use the array form (`traits: ["a", "b"]`) to make precedence explicit.
189189
- **SSR/RSC:** Keep dynamic resolvers pure (same input → same output) to avoid hydration mismatches.
190+
- **Static config:** `windCtrl` configuration is treated as static/immutable. Mutating the config object after creation is not supported.
190191

191192
## License
192193

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect } from "vitest";
2-
import { windCtrl } from "../index";
2+
import { windCtrl } from "./";
33

44
describe("windCtrl", () => {
55
describe("Base classes", () => {

src/index.ts

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,15 @@ function processTraits<TTraits extends Record<string, ClassValue>>(
7575
return [];
7676
}
7777

78-
function processDynamic<TDynamic extends Record<string, DynamicResolver>>(
79-
dynamic: TDynamic,
80-
props: Props<{}, {}, TDynamic>,
78+
function processDynamicEntries(
79+
entries: [string, DynamicResolver][],
80+
props: Record<string, any>,
8181
): { className: ClassValue[]; style: CSSProperties } {
8282
const classNameParts: ClassValue[] = [];
8383
const styles: CSSProperties[] = [];
8484

85-
for (const [key, resolver] of Object.entries(dynamic)) {
86-
const value = props[key as keyof TDynamic];
85+
for (const [key, resolver] of entries) {
86+
const value = props[key];
8787
if (value !== undefined && value !== null) {
8888
const result = resolver(value);
8989
if (typeof result === "string") {
@@ -137,6 +137,16 @@ export function windCtrl<
137137
defaultVariants = {},
138138
} = config;
139139

140+
const resolvedVariants = Object.entries(variants) as [
141+
string,
142+
Record<string, ClassValue>,
143+
][];
144+
const resolvedDynamicEntries = Object.entries(dynamic) as [
145+
string,
146+
DynamicResolver,
147+
][];
148+
const resolvedScopeClasses = processScopes(scopes);
149+
140150
return (props = {} as Props<TVariants, TTraits, TDynamic>) => {
141151
const classNameParts: ClassValue[] = [];
142152
let mergedStyle: CSSProperties = {};
@@ -150,34 +160,33 @@ export function windCtrl<
150160
}
151161

152162
// 2. Variants (with defaultVariants fallback)
153-
if (variants) {
154-
for (const [variantKey, variantOptions] of Object.entries(variants)) {
155-
const propValue =
156-
props[variantKey as keyof typeof props] ??
157-
defaultVariants[variantKey as keyof typeof defaultVariants];
158-
if (propValue && variantOptions[propValue as string]) {
159-
classNameParts.push(variantOptions[propValue as string]);
160-
}
163+
for (const [variantKey, variantOptions] of resolvedVariants) {
164+
const propValue =
165+
props[variantKey as keyof typeof props] ??
166+
defaultVariants[variantKey as keyof typeof defaultVariants];
167+
if (propValue && variantOptions[propValue as string]) {
168+
classNameParts.push(variantOptions[propValue as string]);
161169
}
162170
}
163171

164172
// 3. Traits (higher priority than variants)
165-
if (traits && props.traits) {
166-
const traitClasses = processTraits(traits, props.traits);
167-
classNameParts.push(...traitClasses);
173+
if (props.traits) {
174+
classNameParts.push(...processTraits(traits, props.traits));
168175
}
169176

170177
// 4. Dynamic (highest priority for className)
171-
if (dynamic) {
172-
const dynamicResult = processDynamic(dynamic, props);
178+
if (resolvedDynamicEntries.length) {
179+
const dynamicResult = processDynamicEntries(
180+
resolvedDynamicEntries,
181+
props,
182+
);
173183
classNameParts.push(...dynamicResult.className);
174184
mergedStyle = mergeStyles(mergedStyle, dynamicResult.style);
175185
}
176186

177187
// 5. Scopes (always applied, but don't conflict with other classes)
178-
if (scopes) {
179-
const scopeClasses = processScopes(scopes);
180-
classNameParts.push(...scopeClasses);
188+
if (resolvedScopeClasses.length) {
189+
classNameParts.push(...resolvedScopeClasses);
181190
}
182191

183192
const finalClassName = twMerge(clsx(classNameParts));

0 commit comments

Comments
 (0)