Skip to content

Commit cbf85b4

Browse files
committed
PoC
0 parents  commit cbf85b4

File tree

11 files changed

+2760
-0
lines changed

11 files changed

+2760
-0
lines changed

.gitignore

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# dependencies
2+
node_modules/
3+
4+
# build output
5+
dist/
6+
build/
7+
lib/
8+
9+
# logs
10+
npm-debug.log*
11+
yarn-debug.log*
12+
yarn-error.log*
13+
pnpm-debug.log*
14+
15+
# OS / editor
16+
.DS_Store
17+
Thumbs.db
18+
19+
# editor / IDE
20+
.vscode/
21+
.idea/
22+
23+
# environment variables
24+
.env
25+
.env.*
26+
27+
# coverage
28+
coverage/
29+
30+
# cache
31+
.cache/
32+
.eslintcache

.npmignore

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Source files
2+
src/
3+
examples/
4+
__tests__/
5+
*.test.ts
6+
*.test.tsx
7+
*.spec.ts
8+
*.spec.tsx
9+
10+
# Development files
11+
.vscode/
12+
.idea/
13+
*.swp
14+
*.swo
15+
*~
16+
17+
# Config files
18+
tsconfig.json
19+
vitest.config.ts
20+
.gitignore
21+
.npmignore
22+
23+
# Build artifacts (will be in dist/)
24+
*.tsbuildinfo
25+
26+
# Dependencies
27+
node_modules/
28+
29+
# Test files
30+
coverage/
31+
.nyc_output/
32+
33+
# Misc
34+
.DS_Store
35+
*.log
36+
npm-debug.log*
37+
yarn-debug.log*
38+
yarn-error.log*
39+

README.md

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# WindCtrl
2+
3+
> Next-generation styling library for Tailwind CSS
4+
5+
WindCtrl is a powerful styling library that evolves the concept of Variant APIs (like CVA) by introducing **Traits** for composable states and **Interpolated Variants** for dynamic values, all while maintaining a zero-runtime-dependency philosophy for style injection.
6+
7+
## Features
8+
9+
- 🎨 **Unified API** - Hides the distinction between static Tailwind classes and dynamic inline styles
10+
- 🧩 **Trait System** - Solves combinatorial explosion by treating states as stackable, non-exclusive layers
11+
- 🎯 **Scoped Styling** - Context-aware styling without React Context (RSC friendly)
12+
- 🔒 **Type-Safe** - Full TypeScript support with intelligent type inference
13+
- 📦 **Zero Runtime** - Minimal bundle size with only `clsx` and `tailwind-merge` as dependencies
14+
-**Performance** - Optimized for render performance
15+
16+
## Quick Start
17+
18+
```typescript
19+
import { windCtrl } from "windctrl";
20+
21+
const button = windCtrl({
22+
base: "rounded px-4 py-2 font-medium transition",
23+
variants: {
24+
intent: {
25+
primary: "bg-blue-500 text-white hover:bg-blue-600",
26+
secondary: "bg-gray-500 text-gray-900 hover:bg-gray-600",
27+
},
28+
size: {
29+
sm: "text-sm",
30+
md: "text-base",
31+
lg: "text-lg",
32+
},
33+
},
34+
traits: {
35+
loading: "opacity-50 cursor-wait",
36+
glass: "backdrop-blur bg-white/10",
37+
},
38+
dynamic: {
39+
w: (val) =>
40+
typeof val === "number"
41+
? { style: { width: `${val}px` } }
42+
: `w-${val}`,
43+
},
44+
defaultVariants: {
45+
intent: "primary",
46+
size: "md",
47+
},
48+
});
49+
50+
// Usage
51+
const { className, style } = button({
52+
intent: "primary",
53+
size: "lg",
54+
traits: ["loading", "glass"],
55+
w: 200,
56+
});
57+
```
58+
59+
## Core Concepts
60+
61+
### Dynamic Props (Interpolated Variants)
62+
63+
Dynamic props allow you to pass arbitrary values that can resolve to either Tailwind classes or inline styles, bridging the gap between static classes and dynamic styles.
64+
65+
```typescript
66+
const button = windCtrl({
67+
dynamic: {
68+
w: (val) =>
69+
typeof val === "number"
70+
? { style: { width: `${val}px` } }
71+
: `w-${val}`,
72+
h: (val) =>
73+
typeof val === "number"
74+
? { style: { height: `${val}px` } }
75+
: `h-${val}`,
76+
},
77+
});
78+
79+
// Usage
80+
button({ w: "full" }); // Returns className: "w-full"
81+
button({ w: 200 }); // Returns style: { width: "200px" }
82+
button({ w: 200, h: 100 }); // Returns both className and style
83+
```
84+
85+
### Traits
86+
87+
Traits are stackable, non-exclusive states. Unlike variants, multiple traits can be active simultaneously, solving the combinatorial explosion problem of `compoundVariants`.
88+
89+
```typescript
90+
const button = windCtrl({
91+
traits: {
92+
loading: "opacity-50 cursor-wait",
93+
glass: "backdrop-blur bg-white/10",
94+
disabled: "pointer-events-none",
95+
},
96+
});
97+
98+
// Usage - Array form
99+
button({ traits: ["loading", "glass"] });
100+
101+
// Usage - Object form
102+
button({ traits: { loading: true, glass: true, disabled: false } });
103+
```
104+
105+
### Scopes
106+
107+
Scopes provide context-aware styling without React Context, making it fully compatible with Server Components (RSC).
108+
109+
```typescript
110+
const button = windCtrl({
111+
scopes: {
112+
header: "text-sm",
113+
footer: "text-xs",
114+
},
115+
});
116+
117+
// Usage - Wrap elements with data-scope attribute
118+
<div data-scope="header" className="group/wind-scope">
119+
<button className={button({}).className}>Header Button</button>
120+
</div>
121+
```
122+
123+
The scope classes are automatically prefixed with `group-data-[scope=...]/wind-scope:` to work with Tailwind's group modifier.
124+
125+
126+
### Variants
127+
128+
Variants are mutually exclusive options. Each variant dimension can have multiple options, but only one option per dimension can be active at a time.
129+
130+
```typescript
131+
const button = windCtrl({
132+
variants: {
133+
intent: {
134+
primary: "bg-blue-500",
135+
secondary: "bg-gray-500",
136+
},
137+
size: {
138+
sm: "text-sm",
139+
md: "text-base",
140+
lg: "text-lg",
141+
},
142+
},
143+
});
144+
145+
// Usage
146+
button({ intent: "primary", size: "lg" });
147+
```
148+
149+
## License
150+
151+
The MIT License (MIT)
152+
153+
Copyright (c) 2025 Masaki Morishita

examples/Button.tsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import React from "react";
2+
import { windCtrl } from "../src/index";
3+
import type { ComponentPropsWithoutRef, ElementType } from "react";
4+
5+
const button = windCtrl({
6+
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",
7+
variants: {
8+
intent: {
9+
primary: "bg-blue-500 text-white hover:bg-blue-600",
10+
secondary: "bg-gray-500 text-gray-900 hover:bg-gray-600",
11+
destructive: "bg-red-500 text-white hover:bg-red-600",
12+
outline: "border border-gray-300 bg-transparent hover:bg-gray-100",
13+
ghost: "hover:bg-gray-100",
14+
},
15+
size: {
16+
sm: "h-8 px-3 text-sm",
17+
md: "h-10 px-4 py-2",
18+
lg: "h-12 px-6 text-lg",
19+
},
20+
},
21+
traits: {
22+
loading: "opacity-50 cursor-wait",
23+
glass: "backdrop-blur bg-white/10 border border-white/20",
24+
disabled: "pointer-events-none opacity-50",
25+
},
26+
dynamic: {
27+
w: (val) =>
28+
typeof val === "number"
29+
? { style: { width: `${val}px` } }
30+
: `w-${val}`,
31+
h: (val) =>
32+
typeof val === "number"
33+
? { style: { height: `${val}px` } }
34+
: `h-${val}`,
35+
},
36+
defaultVariants: {
37+
intent: "primary",
38+
size: "md",
39+
},
40+
scopes: {
41+
header: "text-sm",
42+
footer: "text-xs",
43+
},
44+
});
45+
46+
type ButtonProps<T extends ElementType = "button"> = {
47+
as?: T;
48+
intent?: "primary" | "secondary" | "destructive" | "outline" | "ghost";
49+
size?: "sm" | "md" | "lg";
50+
traits?: Array<"loading" | "glass" | "disabled"> | { loading?: boolean; glass?: boolean; disabled?: boolean };
51+
w?: string | number;
52+
h?: string | number;
53+
} & ComponentPropsWithoutRef<T>;
54+
55+
export function Button<T extends ElementType = "button">({
56+
as,
57+
intent,
58+
size,
59+
traits,
60+
w,
61+
h,
62+
className,
63+
style,
64+
children,
65+
...props
66+
}: ButtonProps<T>) {
67+
const Component = as || "button";
68+
const { className: buttonClassName, style: buttonStyle } = button({
69+
intent,
70+
size,
71+
traits,
72+
w,
73+
h,
74+
});
75+
76+
return (
77+
<Component
78+
className={`${buttonClassName} ${className || ""}`}
79+
style={{ ...buttonStyle, ...style }}
80+
{...props}
81+
>
82+
{children}
83+
</Component>
84+
);
85+
}
86+

0 commit comments

Comments
 (0)