Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ It evolves the concept of Variant APIs (like [cva](https://cva.style/)) by intro

Explore the API interactively. I have prepared a **context-aware ChatGPT session** to act as your coding companion. It understands the full WindCtrl API and can generate production-ready code for your specific use cases.

👉 [**Open Interactive Guide in ChatGPT**](<https://chatgpt.com/?q=Please%20explain%20how%20to%20use%20WindCtrl%20and%20compare%20it%20with%20cva%20and%20Tailwind%20Variants%20based%20on%20the%20source%20code%20in%20the%20README%20below.%0A%0A---%0A%0AREADME%3A%0A%0A%23%20WindCtrl%0A%0A%3E%20Advanced%20variant%20API%20for%20Tailwind%20CSS%20with%20stackable%20traits%20and%20interpolated%20dynamic%20styles.%0A%0A%2A%2AWindCtrl%2A%2A%20is%20a%20next-generation%20styling%20utility%20that%20unifies%20static%20Tailwind%20classes%20and%20dynamic%20inline%20styles%20into%20a%20single%2C%20type-safe%20interface.%0A%0AIt%20evolves%20the%20concept%20of%20Variant%20APIs%20(like%20%5Bcva%5D(https%3A%2F%2Fcva.style%2F))%20by%20introducing%20%2A%2AStackable%20Traits%2A%2A%20to%20solve%20combinatorial%20explosion%20and%20%2A%2AInterpolated%20Variants%2A%2A%20for%20seamless%20dynamic%20value%20handling%E2%80%94all%20while%20maintaining%20a%20minimal%20runtime%20footprint%20optimized%20for%20Tailwind%27s%20JIT%20compiler.%0A%0A%23%23%20Features%0A%0A-%20%F0%9F%8E%A8%20%2A%2AUnified%20API%2A%2A%20-%20Seamlessly%20blends%20static%20Tailwind%20classes%20and%20dynamic%20inline%20styles%20into%20one%20cohesive%20interface.%0A-%20%F0%9F%A7%A9%20%2A%2ATrait%20System%2A%2A%20-%20Solves%20combinatorial%20explosion%20by%20treating%20states%20as%20stackable%2C%20non-exclusive%20layers.%0A-%20%F0%9F%8E%AF%20%2A%2AScoped%20Styling%2A%2A%20-%20Context-aware%20styling%20using%20data%20attributes%20-%20no%20React%20Context%20required%20(RSC%20friendly).%0A-%20%E2%9A%A1%20%2A%2AJIT%20Optimized%2A%2A%20-%20Prevents%20CSS%20bundle%20bloat%20by%20intelligently%20routing%20arbitrary%20values%20to%20inline%20styles.%0A-%20%F0%9F%94%92%20%2A%2AType-Safe%2A%2A%20-%20Best-in-class%20TypeScript%20support%20with%20automatic%20prop%20inference.%0A-%20%F0%9F%93%A6%20%2A%2AMinimal%20Overhead%2A%2A%20-%20Ultra-lightweight%20runtime%20with%20only%20%60clsx%60%20and%20%60tailwind-merge%60%20as%20dependencies.%0A%0A%23%23%20Installation%0A%0A%60%60%60bash%0Anpm%20install%20windctrl%0A%60%60%60%0A%0A%23%23%20Quick%20Start%0A%0A%60%60%60typescript%0Aimport%20%7B%20windCtrl%20%7D%20from%20%22windctrl%22%3B%0A%0Aconst%20button%20%3D%20windCtrl(%7B%0A%20%20base%3A%20%22rounded%20px-4%20py-2%20font-medium%20transition%20duration-200%22%2C%0A%20%20variants%3A%20%7B%0A%20%20%20%20intent%3A%20%7B%0A%20%20%20%20%20%20primary%3A%20%22bg-blue-500%20text-white%20hover%3Abg-blue-600%22%2C%0A%20%20%20%20%20%20secondary%3A%20%22bg-gray-200%20text-gray-900%20hover%3Abg-gray-300%22%2C%0A%20%20%20%20%7D%2C%0A%20%20%20%20size%3A%20%7B%0A%20%20%20%20%20%20sm%3A%20%22text-sm%20h-8%22%2C%0A%20%20%20%20%20%20md%3A%20%22text-base%20h-10%22%2C%0A%20%20%20%20%20%20lg%3A%20%22text-lg%20h-12%22%2C%0A%20%20%20%20%7D%2C%0A%20%20%7D%2C%0A%20%20traits%3A%20%7B%0A%20%20%20%20loading%3A%20%22opacity-70%20cursor-wait%20pointer-events-none%22%2C%0A%20%20%20%20glass%3A%20%22backdrop-blur-md%20bg-white%2F10%20border%20border-white%2F20%20shadow-xl%22%2C%0A%20%20%7D%2C%0A%20%20dynamic%3A%20%7B%0A%20%20%20%20w%3A%20(val)%20%3D%3E%0A%20%20%20%20%20%20typeof%20val%20%3D%3D%3D%20%22number%22%20%3F%20%7B%20style%3A%20%7B%20width%3A%20%60%24%7Bval%7Dpx%60%20%7D%20%7D%20%3A%20val%2C%0A%20%20%7D%2C%0A%20%20defaultVariants%3A%20%7B%0A%20%20%20%20intent%3A%20%22primary%22%2C%0A%20%20%20%20size%3A%20%22md%22%2C%0A%20%20%7D%2C%0A%7D)%3B%0A%0A%2F%2F%20Usage%20Example%0A%0A%2F%2F%201.%20Standard%20usage%0Abutton(%7B%20intent%3A%20%22primary%22%2C%20size%3A%20%22lg%22%20%7D)%3B%0A%0A%2F%2F%202.%20Using%20Traits%20(Stackable%20states)%0Abutton(%7B%20traits%3A%20%5B%22glass%22%2C%20%22loading%22%5D%20%7D)%3B%0A%0A%2F%2F%203.%20Unified%20API%20for%20dynamic%20values%0A%2F%2F%20Pass%20a%20number%20for%20arbitrary%20px%20value%20(Inline%20Style)%0Abutton(%7B%20w%3A%20350%20%7D)%3B%0A%2F%2F%20Pass%20a%20string%20for%20Tailwind%20utility%20(Static%20Class)%0Abutton(%7B%20w%3A%20%22w-full%22%20%7D)%3B%0A%60%>)
👉 [**Open Interactive Guide in ChatGPT**](<https://chatgpt.com/?q=Please%20explain%20how%20to%20use%20WindCtrl%20and%20compare%20it%20with%20cva%20and%20Tailwind%20Variants%20based%20on%20the%20source%20code%20in%20the%20README%20below.%0A%0A---%0A%0AREADME%3A%0A%0A%23%20WindCtrl%0A%0A%3E%20Advanced%20variant%20API%20for%20Tailwind%20CSS%20with%20stackable%20traits%20and%20interpolated%20dynamic%20styles.%0A%0A%2A%2AWindCtrl%2A%2A%20is%20a%20next-generation%20styling%20utility%20that%20unifies%20static%20Tailwind%20classes%20and%20dynamic%20inline%20styles%20into%20a%20single%2C%20type-safe%20interface.%0A%0AIt%20evolves%20the%20concept%20of%20Variant%20APIs%20(like%20%5Bcva%5D(https%3A%2F%2Fcva.style%2F))%20by%20introducing%20%2A%2AStackable%20Traits%2A%2A%20to%20solve%20combinatorial%20explosion%20and%20%2A%2AInterpolated%20Variants%2A%2A%20for%20seamless%20dynamic%20value%20handling%E2%80%94all%20while%20maintaining%20a%20minimal%20runtime%20footprint%20optimized%20for%20Tailwind%27s%20JIT%20compiler.%0A%0A%23%23%20Features%0A%0A-%20%F0%9F%8E%A8%20%2A%2AUnified%20API%2A%2A%20-%20Seamlessly%20blends%20static%20Tailwind%20classes%20and%20dynamic%20inline%20styles%20into%20one%20cohesive%20interface.%0A-%20%F0%9F%A7%A9%20%2A%2ATrait%20System%2A%2A%20-%20Solves%20combinatorial%20explosion%20by%20treating%20states%20as%20stackable%2C%20non-exclusive%20layers.%0A-%20%F0%9F%8E%AF%20%2A%2AScoped%20Styling%2A%2A%20-%20Context-aware%20styling%20using%20data%20attributes%20-%20no%20React%20Context%20required%20(RSC%20friendly).%0A-%20%E2%9A%A1%20%2A%2AJIT%20Optimized%2A%2A%20-%20Prevents%20CSS%20bundle%20bloat%20by%20intelligently%20routing%20arbitrary%20values%20to%20inline%20styles.%0A-%20%F0%9F%94%92%20%2A%2AType-Safe%2A%2A%20-%20Best-in-class%20TypeScript%20support%20with%20automatic%20prop%20inference.%0A-%20%F0%9F%93%A6%20%2A%2AMinimal%20Overhead%2A%2A%20-%20Ultra-lightweight%20runtime%20with%20only%20%60clsx%60%20and%20%60tailwind-merge%60%20as%20dependencies.%0A%0A%23%23%20Installation%0A%0A%60%60%60bash%0Anpm%20install%20windctrl%0A%60%60%60%0A%0A%23%23%20Quick%20Start%0A%0A%60%60%60typescript%0Aimport%20%7B%20windctrl%20%7D%20from%20%22windctrl%22%3B%0A%0Aconst%20button%20%3D%20windctrl(%7B%0A%20%20base%3A%20%22rounded%20px-4%20py-2%20font-medium%20transition%20duration-200%22%2C%0A%20%20variants%3A%20%7B%0A%20%20%20%20intent%3A%20%7B%0A%20%20%20%20%20%20primary%3A%20%22bg-blue-500%20text-white%20hover%3Abg-blue-600%22%2C%0A%20%20%20%20%20%20secondary%3A%20%22bg-gray-200%20text-gray-900%20hover%3Abg-gray-300%22%2C%0A%20%20%20%20%7D%2C%0A%20%20%20%20size%3A%20%7B%0A%20%20%20%20%20%20sm%3A%20%22text-sm%20h-8%22%2C%0A%20%20%20%20%20%20md%3A%20%22text-base%20h-10%22%2C%0A%20%20%20%20%20%20lg%3A%20%22text-lg%20h-12%22%2C%0A%20%20%20%20%7D%2C%0A%20%20%7D%2C%0A%20%20traits%3A%20%7B%0A%20%20%20%20loading%3A%20%22opacity-70%20cursor-wait%20pointer-events-none%22%2C%0A%20%20%20%20glass%3A%20%22backdrop-blur-md%20bg-white%2F10%20border%20border-white%2F20%20shadow-xl%22%2C%0A%20%20%7D%2C%0A%20%20dynamic%3A%20%7B%0A%20%20%20%20w%3A%20(val)%20%3D%3E%0A%20%20%20%20%20%20typeof%20val%20%3D%3D%3D%20%22number%22%20%3F%20%7B%20style%3A%20%7B%20width%3A%20%60%24%7Bval%7Dpx%60%20%7D%20%7D%20%3A%20val%2C%0A%20%20%7D%2C%0A%20%20defaultVariants%3A%20%7B%0A%20%20%20%20intent%3A%20%22primary%22%2C%0A%20%20%20%20size%3A%20%22md%22%2C%0A%20%20%7D%2C%0A%7D)%3B%0A%0A%2F%2F%20Usage%20Example%0A%0A%2F%2F%201.%20Standard%20usage%0Abutton(%7B%20intent%3A%20%22primary%22%2C%20size%3A%20%22lg%22%20%7D)%3B%0A%0A%2F%2F%202.%20Using%20Traits%20(Stackable%20states)%0Abutton(%7B%20traits%3A%20%5B%22glass%22%2C%20%22loading%22%5D%20%7D)%3B%0A%0A%2F%2F%203.%20Unified%20API%20for%20dynamic%20values%0A%2F%2F%20Pass%20a%20number%20for%20arbitrary%20px%20value%20(Inline%20Style)%0Abutton(%7B%20w%3A%20350%20%7D)%3B%0A%2F%2F%20Pass%20a%20string%20for%20Tailwind%20utility%20(Static%20Class)%0Abutton(%7B%20w%3A%20%22w-full%22%20%7D)%3B%0A%60%>)

> **Note:** The entire source code is just ~200 lines - small enough to fit entirely in your AI's context window for perfect understanding! :D

Expand All @@ -34,9 +34,9 @@ npm install windctrl
## Quick Start

```typescript
import { windCtrl } from "windctrl";
import { windctrl } from "windctrl";

const button = windCtrl({
const button = windctrl({
base: "rounded px-4 py-2 font-medium transition duration-200",
variants: {
intent: {
Expand Down Expand Up @@ -91,7 +91,7 @@ This is **JIT-friendly by design**, as long as the class strings you return are
For truly unbounded values (e.g. pixel sizes), prefer returning style to avoid relying on arbitrary-value class generation.

```typescript
const button = windCtrl({
const button = windctrl({
dynamic: {
// Recommended pattern:
// - Numbers -> inline styles (unbounded values)
Expand All @@ -118,7 +118,7 @@ When multiple traits generate conflicting utilities, Tailwind’s “last one wi
If ordering matters, prefer the **array form** to make precedence explicit.

```typescript
const button = windCtrl({
const button = windctrl({
traits: {
loading: "opacity-50 cursor-wait",
glass: "backdrop-blur-md bg-white/10 border border-white/20",
Expand All @@ -138,7 +138,7 @@ button({ traits: { loading: isLoading, glass: true } });
Scopes enable **context-aware styling** without relying on React Context or client-side JavaScript. This makes them fully compatible with React Server Components (RSC). They utilize Tailwind's group modifier logic under the hood.

```typescript
const button = windCtrl({
const button = windctrl({
scopes: {
header: "text-sm py-1",
footer: "text-xs text-gray-500",
Expand All @@ -160,7 +160,7 @@ The scope classes are automatically prefixed with `group-data-[scope=...]/wind-s
Variants represent mutually exclusive design choices (e.g., `primary` vs `secondary`). They serve as the foundation of your component's design system.

```typescript
const button = windCtrl({
const button = windctrl({
variants: {
intent: {
primary: "bg-blue-500 text-white hover:bg-blue-600",
Expand All @@ -187,7 +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.
- **Static config:** `windctrl` configuration is treated as static/immutable. Mutating the config object after creation is not supported.

## License

Expand Down
4 changes: 2 additions & 2 deletions examples/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from "react";
import { windCtrl } from "../src/index";
import { windctrl } from "../src/index";
import type { ComponentPropsWithoutRef, ElementType } from "react";

const button = windCtrl({
const button = windctrl({
base: "inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
variants: {
intent: {
Expand Down
70 changes: 38 additions & 32 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { describe, it, expect } from "vitest";
import { windCtrl } from "./";
import { windctrl, wc } from "./";

describe("windCtrl", () => {
describe("wc", () => {
it("should be the same as windctrl", () => {
expect(wc).toBe(windctrl);
});
});

describe("windctrl", () => {
describe("Base classes", () => {
it("should apply base classes when provided", () => {
const button = windCtrl({
const button = windctrl({
base: "rounded px-4 py-2",
});

Expand All @@ -16,7 +22,7 @@ describe("windCtrl", () => {
});

it("should work without base classes", () => {
const button = windCtrl({});
const button = windctrl({});

const result = button({});
expect(result.className).toBe("");
Expand All @@ -26,7 +32,7 @@ describe("windCtrl", () => {

describe("Variants", () => {
it("should apply variant classes based on prop value", () => {
const button = windCtrl({
const button = windctrl({
base: "rounded",
variants: {
intent: {
Expand All @@ -48,7 +54,7 @@ describe("windCtrl", () => {
});

it("should handle multiple variant dimensions", () => {
const button = windCtrl({
const button = windctrl({
variants: {
size: {
sm: "text-sm",
Expand All @@ -68,7 +74,7 @@ describe("windCtrl", () => {
});

it("should not apply variant classes when prop is not provided", () => {
const button = windCtrl({
const button = windctrl({
variants: {
intent: {
primary: "bg-blue-500",
Expand All @@ -85,7 +91,7 @@ describe("windCtrl", () => {

describe("Default variants", () => {
it("should apply default variant values when prop is not provided", () => {
const button = windCtrl({
const button = windctrl({
variants: {
intent: {
primary: "bg-blue-500",
Expand All @@ -102,7 +108,7 @@ describe("windCtrl", () => {
});

it("should allow overriding default variants", () => {
const button = windCtrl({
const button = windctrl({
variants: {
intent: {
primary: "bg-blue-500",
Expand All @@ -120,7 +126,7 @@ describe("windCtrl", () => {
});

it("should handle multiple default variants", () => {
const button = windCtrl({
const button = windctrl({
variants: {
size: {
sm: "text-sm",
Expand All @@ -145,7 +151,7 @@ describe("windCtrl", () => {

describe("Traits", () => {
it("should apply trait classes when provided as array", () => {
const button = windCtrl({
const button = windctrl({
base: "rounded",
traits: {
loading: "opacity-50 cursor-wait",
Expand All @@ -162,7 +168,7 @@ describe("windCtrl", () => {
});

it("should apply trait classes when provided as object", () => {
const button = windCtrl({
const button = windctrl({
traits: {
loading: "opacity-50",
glass: "backdrop-blur",
Expand All @@ -179,7 +185,7 @@ describe("windCtrl", () => {
});

it("should handle empty traits array", () => {
const button = windCtrl({
const button = windctrl({
base: "rounded",
traits: {
loading: "opacity-50",
Expand All @@ -192,7 +198,7 @@ describe("windCtrl", () => {
});

it("should handle empty traits object", () => {
const button = windCtrl({
const button = windctrl({
base: "rounded",
traits: {
loading: "opacity-50",
Expand All @@ -205,7 +211,7 @@ describe("windCtrl", () => {
});

it("should apply multiple traits orthogonally", () => {
const button = windCtrl({
const button = windctrl({
traits: {
loading: "opacity-50",
glass: "backdrop-blur",
Expand All @@ -224,7 +230,7 @@ describe("windCtrl", () => {

describe("Dynamic (Interpolated Variants)", () => {
it("should apply className when dynamic resolver returns string", () => {
const button = windCtrl({
const button = windctrl({
dynamic: {
w: (val) => (typeof val === "number" ? `w-[${val}px]` : `w-${val}`),
},
Expand All @@ -236,7 +242,7 @@ describe("windCtrl", () => {
});

it("should apply style when dynamic resolver returns object with style", () => {
const button = windCtrl({
const button = windctrl({
dynamic: {
w: (val) =>
typeof val === "number"
Expand All @@ -250,7 +256,7 @@ describe("windCtrl", () => {
});

it("should merge className and style when dynamic resolver returns both", () => {
const button = windCtrl({
const button = windctrl({
base: "rounded",
dynamic: {
color: (val) => ({
Expand All @@ -266,7 +272,7 @@ describe("windCtrl", () => {
});

it("should handle multiple dynamic props", () => {
const button = windCtrl({
const button = windctrl({
dynamic: {
w: (val) =>
typeof val === "number"
Expand All @@ -284,7 +290,7 @@ describe("windCtrl", () => {
});

it("should handle mixed dynamic props (string and number)", () => {
const button = windCtrl({
const button = windctrl({
dynamic: {
w: (val) =>
typeof val === "number"
Expand All @@ -304,7 +310,7 @@ describe("windCtrl", () => {

describe("Scopes", () => {
it("should apply scope classes with group-data selector", () => {
const button = windCtrl({
const button = windctrl({
base: "rounded",
scopes: {
header: "text-sm",
Expand All @@ -322,7 +328,7 @@ describe("windCtrl", () => {
});

it("should combine scopes with base classes", () => {
const button = windCtrl({
const button = windctrl({
base: "px-4 py-2",
scopes: {
header: "text-sm",
Expand All @@ -340,7 +346,7 @@ describe("windCtrl", () => {

describe("Priority and Integration", () => {
it("should apply classes in correct priority: Dynamic > Traits > Variants > Base", () => {
const button = windCtrl({
const button = windctrl({
base: "base-class",
variants: {
intent: {
Expand Down Expand Up @@ -368,7 +374,7 @@ describe("windCtrl", () => {
});

it("should handle complex real-world scenario", () => {
const button = windCtrl({
const button = windctrl({
base: "rounded px-4 py-2 font-medium transition",
variants: {
intent: {
Expand Down Expand Up @@ -427,7 +433,7 @@ describe("windCtrl", () => {
});

it("should merge conflicting Tailwind classes (last one wins)", () => {
const button = windCtrl({
const button = windctrl({
base: "text-red-500",
variants: {
intent: {
Expand All @@ -444,14 +450,14 @@ describe("windCtrl", () => {

describe("Edge cases", () => {
it("should handle empty configuration", () => {
const button = windCtrl({});
const button = windctrl({});
const result = button({});
expect(result.className).toBe("");
expect(result.style).toEqual(undefined);
});

it("should handle undefined props gracefully", () => {
const button = windCtrl({
const button = windctrl({
variants: {
intent: {
primary: "bg-blue-500",
Expand All @@ -464,7 +470,7 @@ describe("windCtrl", () => {
});

it("should handle null props gracefully", () => {
const button = windCtrl({
const button = windctrl({
variants: {
intent: {
primary: "bg-blue-500",
Expand All @@ -477,7 +483,7 @@ describe("windCtrl", () => {
});

it("should handle traits with invalid keys gracefully", () => {
const button = windCtrl({
const button = windctrl({
traits: {
loading: "opacity-50",
},
Expand All @@ -490,7 +496,7 @@ describe("windCtrl", () => {

describe("Type safety", () => {
it("should infer variant prop types correctly", () => {
const button = windCtrl({
const button = windctrl({
variants: {
intent: {
primary: "bg-blue-500",
Expand All @@ -507,7 +513,7 @@ describe("windCtrl", () => {
});

it("should infer trait keys correctly", () => {
const button = windCtrl({
const button = windctrl({
traits: {
loading: "opacity-50",
glass: "backdrop-blur",
Expand All @@ -526,7 +532,7 @@ describe("windCtrl", () => {
});

it("should infer dynamic prop types correctly", () => {
const button = windCtrl({
const button = windctrl({
dynamic: {
w: (val) =>
typeof val === "number"
Expand Down
Loading