windctrl 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -51
- package/dist/index.d.ts +33 -6
- package/dist/index.js +129 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
**WindCtrl** is a next-generation styling utility that unifies static Tailwind classes and dynamic inline styles into a single, type-safe interface.
|
|
6
6
|
|
|
7
|
-
It
|
|
7
|
+
It builds on existing variant APIs (like [cva](https://cva.style/)) and introduces **Stackable Traits** to avoid combinatorial explosion, as well as **Interpolated Variants** for seamless dynamic styling.
|
|
8
|
+
All of this is achieved with a minimal runtime footprint and full compatibility with Tailwind's JIT compiler.
|
|
8
9
|
|
|
9
10
|
## Features
|
|
10
11
|
|
|
11
|
-
- 🎨 **Unified API** - Seamlessly blends static Tailwind classes and dynamic inline styles into one cohesive interface.
|
|
12
12
|
- 🧩 **Trait System** - Solves combinatorial explosion by treating states as stackable, non-exclusive layers.
|
|
13
|
-
-
|
|
13
|
+
- 🎨 **Unified API** - Seamlessly blends static Tailwind classes and dynamic inline styles into one cohesive interface.
|
|
14
14
|
- ⚡ **JIT Conscious** - Designed for Tailwind JIT: utilities stay as class strings, while truly dynamic values can be expressed as inline styles.
|
|
15
|
+
- 🎯 **Scoped Styling** - Context-aware styling using data attributes - no React Context required (RSC friendly).
|
|
15
16
|
- 🔒 **Type-Safe** - Best-in-class TypeScript support with automatic prop inference.
|
|
16
17
|
- 📦 **Minimal Overhead** - Ultra-lightweight runtime with only `clsx` and `tailwind-merge` as dependencies.
|
|
17
18
|
|
|
@@ -24,7 +25,7 @@ npm install windctrl
|
|
|
24
25
|
## Quick Start
|
|
25
26
|
|
|
26
27
|
```typescript
|
|
27
|
-
import { windctrl } from "windctrl";
|
|
28
|
+
import { windctrl, dynamic as d } from "windctrl";
|
|
28
29
|
|
|
29
30
|
const button = windctrl({
|
|
30
31
|
base: "rounded px-4 py-2 font-medium transition duration-200",
|
|
@@ -44,8 +45,7 @@ const button = windctrl({
|
|
|
44
45
|
glass: "backdrop-blur-md bg-white/10 border border-white/20 shadow-xl",
|
|
45
46
|
},
|
|
46
47
|
dynamic: {
|
|
47
|
-
w: (
|
|
48
|
-
typeof val === "number" ? { style: { width: `${val}px` } } : val,
|
|
48
|
+
w: d.px("width"),
|
|
49
49
|
},
|
|
50
50
|
defaultVariants: {
|
|
51
51
|
intent: "primary",
|
|
@@ -70,34 +70,33 @@ button({ w: "w-full" });
|
|
|
70
70
|
|
|
71
71
|
## Core Concepts
|
|
72
72
|
|
|
73
|
-
###
|
|
74
|
-
|
|
75
|
-
Interpolated variants provide a **Unified API** that bridges static Tailwind classes and dynamic inline styles. A dynamic resolver can return either:
|
|
76
|
-
|
|
77
|
-
- a **Tailwind class string** (static utility), or
|
|
78
|
-
- an object containing **className and/or style** (inline styles and optional utilities)
|
|
73
|
+
### Variants
|
|
79
74
|
|
|
80
|
-
|
|
81
|
-
For truly unbounded values (e.g. pixel sizes), prefer returning style to avoid relying on arbitrary-value class generation.
|
|
75
|
+
Variants represent mutually exclusive design choices (e.g., `primary` vs `secondary`). They serve as the foundation of your component's design system.
|
|
82
76
|
|
|
83
77
|
```typescript
|
|
84
78
|
const button = windctrl({
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
79
|
+
variants: {
|
|
80
|
+
intent: {
|
|
81
|
+
primary: "bg-blue-500 text-white hover:bg-blue-600",
|
|
82
|
+
secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200",
|
|
83
|
+
},
|
|
84
|
+
size: {
|
|
85
|
+
sm: "text-sm h-8 px-3",
|
|
86
|
+
md: "text-base h-10 px-4",
|
|
87
|
+
lg: "text-lg h-12 px-6",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
defaultVariants: {
|
|
91
|
+
intent: "primary",
|
|
92
|
+
size: "md",
|
|
91
93
|
},
|
|
92
94
|
});
|
|
93
95
|
|
|
94
96
|
// Usage
|
|
95
|
-
button({
|
|
96
|
-
button({ w: 200 }); // -> style includes { width: "200px" } (dynamic value)
|
|
97
|
+
button({ intent: "primary", size: "lg" });
|
|
97
98
|
```
|
|
98
99
|
|
|
99
|
-
> **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.
|
|
100
|
-
|
|
101
100
|
### Traits (Stackable States)
|
|
102
101
|
|
|
103
102
|
Traits are non-exclusive, stackable layers of state. Unlike `variants` (which are mutually exclusive), multiple traits can be active simultaneously. This declarative approach solves the "combinatorial explosion" problem often seen with `compoundVariants`.
|
|
@@ -123,55 +122,141 @@ button({ traits: ["loading", "glass"] });
|
|
|
123
122
|
button({ traits: { loading: isLoading, glass: true } });
|
|
124
123
|
```
|
|
125
124
|
|
|
126
|
-
###
|
|
125
|
+
### Slots (Compound Components)
|
|
127
126
|
|
|
128
|
-
|
|
127
|
+
Slots allow you to define styles for **sub-elements** (e.g., icon, label) within a single component definition. Each slot returns its own class string, enabling clean compound component patterns.
|
|
128
|
+
|
|
129
|
+
Slots are completely optional and additive. You can start with a single-root component and introduce slots only when needed.
|
|
130
|
+
If a slot is never defined, it simply won't appear in the result.
|
|
129
131
|
|
|
130
132
|
```typescript
|
|
131
133
|
const button = windctrl({
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
base: {
|
|
135
|
+
root: "inline-flex items-center gap-2 rounded px-4 py-2",
|
|
136
|
+
slots: {
|
|
137
|
+
icon: "shrink-0",
|
|
138
|
+
label: "truncate",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
variants: {
|
|
142
|
+
size: {
|
|
143
|
+
sm: {
|
|
144
|
+
root: "h-8 text-sm",
|
|
145
|
+
slots: { icon: "h-3 w-3" },
|
|
146
|
+
},
|
|
147
|
+
md: {
|
|
148
|
+
root: "h-10 text-base",
|
|
149
|
+
slots: { icon: "h-4 w-4" },
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
traits: {
|
|
154
|
+
loading: {
|
|
155
|
+
root: "opacity-70 pointer-events-none",
|
|
156
|
+
slots: { icon: "animate-spin" },
|
|
157
|
+
},
|
|
135
158
|
},
|
|
159
|
+
defaultVariants: { size: "md" },
|
|
136
160
|
});
|
|
137
161
|
|
|
138
162
|
// Usage
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
163
|
+
const { className, slots } = button({ size: "sm", traits: ["loading"] });
|
|
164
|
+
|
|
165
|
+
// Apply to elements
|
|
166
|
+
<button className={className}>
|
|
167
|
+
<Icon className={slots?.icon} />
|
|
168
|
+
<span className={slots?.label}>Click me</span>
|
|
169
|
+
</button>
|
|
144
170
|
```
|
|
145
171
|
|
|
146
|
-
|
|
172
|
+
Slots follow the same priority rules as root classes: **Base < Variants < Traits**, with `tailwind-merge` handling conflicts.
|
|
147
173
|
|
|
148
|
-
|
|
174
|
+
Unlike slot-based APIs that require declaring all slots upfront, WindCtrl allows slots to emerge naturally from variants and traits.
|
|
149
175
|
|
|
150
|
-
|
|
176
|
+
### Interpolated Variants (Dynamic Props)
|
|
177
|
+
|
|
178
|
+
Interpolated variants provide a **Unified API** that bridges static Tailwind classes and dynamic inline styles. A dynamic resolver can return either:
|
|
179
|
+
|
|
180
|
+
- a **Tailwind class string** (static utility), or
|
|
181
|
+
- an object containing **className and/or style** (inline styles and optional utilities)
|
|
182
|
+
|
|
183
|
+
This is **JIT-friendly by design**, as long as the class strings you return are statically enumerable (i.e. appear in your source code).
|
|
184
|
+
For truly unbounded values (e.g. pixel sizes), prefer returning style to avoid relying on arbitrary-value class generation.
|
|
185
|
+
|
|
186
|
+
#### Dynamic Presets
|
|
187
|
+
|
|
188
|
+
WindCtrl provides built-in presets for common dynamic patterns:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { windctrl, dynamic as d } from "windctrl";
|
|
192
|
+
|
|
193
|
+
const box = windctrl({
|
|
194
|
+
dynamic: {
|
|
195
|
+
// d.px() - pixel values (width, height, top, left, etc.)
|
|
196
|
+
w: d.px("width"),
|
|
197
|
+
h: d.px("height"),
|
|
198
|
+
|
|
199
|
+
// d.num() - unitless numbers (zIndex, flexGrow, order, etc.)
|
|
200
|
+
z: d.num("zIndex"),
|
|
201
|
+
|
|
202
|
+
// d.opacity() - opacity values
|
|
203
|
+
fade: d.opacity(),
|
|
204
|
+
|
|
205
|
+
// d.var() - CSS custom properties
|
|
206
|
+
x: d.var("--translate-x", { unit: "px" }),
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Usage
|
|
211
|
+
box({ w: 200 }); // -> style: { width: "200px" }
|
|
212
|
+
box({ w: "w-full" }); // -> className: "w-full"
|
|
213
|
+
box({ z: 50 }); // -> style: { zIndex: 50 }
|
|
214
|
+
box({ fade: 0.5 }); // -> style: { opacity: 0.5 }
|
|
215
|
+
box({ x: 10 }); // -> style: { "--translate-x": "10px" }
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
#### Custom Resolvers
|
|
219
|
+
|
|
220
|
+
You can also write custom resolvers for more complex logic:
|
|
151
221
|
|
|
152
222
|
```typescript
|
|
153
223
|
const button = windctrl({
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
},
|
|
159
|
-
size: {
|
|
160
|
-
sm: "text-sm h-8 px-3",
|
|
161
|
-
md: "text-base h-10 px-4",
|
|
162
|
-
lg: "text-lg h-12 px-6",
|
|
163
|
-
},
|
|
224
|
+
dynamic: {
|
|
225
|
+
// Custom resolver example
|
|
226
|
+
w: (val) =>
|
|
227
|
+
typeof val === "number" ? { style: { width: `${val}px` } } : val,
|
|
164
228
|
},
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Usage
|
|
232
|
+
button({ w: "w-full" }); // -> className includes "w-full" (static utility)
|
|
233
|
+
button({ w: 200 }); // -> style includes { width: "200px" } (dynamic value)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
> **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.
|
|
237
|
+
|
|
238
|
+
### Scopes (RSC Support)
|
|
239
|
+
|
|
240
|
+
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.
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
const button = windctrl({
|
|
244
|
+
scopes: {
|
|
245
|
+
header: "text-sm py-1",
|
|
246
|
+
footer: "text-xs text-gray-500",
|
|
168
247
|
},
|
|
169
248
|
});
|
|
170
249
|
|
|
171
250
|
// Usage
|
|
172
|
-
|
|
251
|
+
// 1. Wrap the parent with `data-windctrl-scope` and `group/windctrl-scope`
|
|
252
|
+
// 2. The button automatically adapts its style based on the parent
|
|
253
|
+
<div data-windctrl-scope="header" className="group/windctrl-scope">
|
|
254
|
+
<button className={button().className}>Header Button</button>
|
|
255
|
+
</div>
|
|
173
256
|
```
|
|
174
257
|
|
|
258
|
+
The scope classes are automatically prefixed with `group-data-[windctrl-scope=...]/windctrl-scope:` to target the parent's data attribute.
|
|
259
|
+
|
|
175
260
|
## Gotchas
|
|
176
261
|
|
|
177
262
|
- **Tailwind JIT:** Tailwind only generates CSS for class names it can statically detect. Avoid constructing class strings dynamically unless you safelist them.
|
package/dist/index.d.ts
CHANGED
|
@@ -4,9 +4,29 @@ type DynamicResolverResult = string | {
|
|
|
4
4
|
className?: string;
|
|
5
5
|
style?: CSSProperties;
|
|
6
6
|
};
|
|
7
|
-
type DynamicResolver = (value:
|
|
8
|
-
type
|
|
9
|
-
|
|
7
|
+
type DynamicResolver<T = any> = (value: T) => DynamicResolverResult;
|
|
8
|
+
type PxProp = "width" | "height" | "minWidth" | "maxWidth" | "minHeight" | "maxHeight" | "top" | "right" | "bottom" | "left";
|
|
9
|
+
type NumProp = "zIndex" | "flexGrow" | "flexShrink" | "order";
|
|
10
|
+
type VarUnit = "px" | "%" | "deg" | "ms";
|
|
11
|
+
declare function px(prop: PxProp): DynamicResolver<number | string>;
|
|
12
|
+
declare function num(prop: NumProp): DynamicResolver<number | string>;
|
|
13
|
+
declare function opacity(): DynamicResolver<number | string>;
|
|
14
|
+
declare function cssVar(name: `--${string}`, options?: {
|
|
15
|
+
unit?: VarUnit;
|
|
16
|
+
}): DynamicResolver<number | string>;
|
|
17
|
+
export declare const dynamic: {
|
|
18
|
+
px: typeof px;
|
|
19
|
+
num: typeof num;
|
|
20
|
+
opacity: typeof opacity;
|
|
21
|
+
var: typeof cssVar;
|
|
22
|
+
};
|
|
23
|
+
type SlotAwareObject = {
|
|
24
|
+
root?: ClassValue;
|
|
25
|
+
slots?: Record<string, ClassValue>;
|
|
26
|
+
};
|
|
27
|
+
type SlotAwareValue = ClassValue | SlotAwareObject;
|
|
28
|
+
type Config<TVariants extends Record<string, Record<string, SlotAwareValue>> = {}, TTraits extends Record<string, SlotAwareValue> = {}, TDynamic extends Record<string, DynamicResolver> = {}, TScopes extends Record<string, ClassValue> = {}> = {
|
|
29
|
+
base?: SlotAwareValue;
|
|
10
30
|
variants?: TVariants;
|
|
11
31
|
traits?: TTraits;
|
|
12
32
|
dynamic?: TDynamic;
|
|
@@ -15,17 +35,24 @@ type Config<TVariants extends Record<string, Record<string, ClassValue>> = {}, T
|
|
|
15
35
|
[K in keyof TVariants]?: keyof TVariants[K] extends string ? keyof TVariants[K] : never;
|
|
16
36
|
};
|
|
17
37
|
};
|
|
18
|
-
type Props<TVariants extends Record<string, Record<string,
|
|
38
|
+
type Props<TVariants extends Record<string, Record<string, SlotAwareValue>> = {}, TTraits extends Record<string, SlotAwareValue> = {}, TDynamic extends Record<string, DynamicResolver> = {}> = {
|
|
19
39
|
[K in keyof TVariants]?: keyof TVariants[K] extends string ? keyof TVariants[K] : never;
|
|
20
40
|
} & {
|
|
21
41
|
traits?: Array<keyof TTraits extends string ? keyof TTraits : never> | Partial<Record<keyof TTraits extends string ? keyof TTraits : never, boolean>>;
|
|
22
42
|
} & {
|
|
23
43
|
[K in keyof TDynamic]?: Parameters<TDynamic[K]>[0];
|
|
24
44
|
};
|
|
25
|
-
type
|
|
45
|
+
type SlotsOfValue<V> = V extends {
|
|
46
|
+
slots?: infer S;
|
|
47
|
+
} ? S extends Record<string, any> ? keyof S : never : never;
|
|
48
|
+
type VariantOptionValues<T> = T extends Record<string, Record<string, infer V>> ? V : never;
|
|
49
|
+
type TraitValues<T> = T extends Record<string, infer V> ? V : never;
|
|
50
|
+
type SlotKeys<TBase, TVariants extends Record<string, Record<string, any>>, TTraits extends Record<string, any>> = Extract<SlotsOfValue<TBase> | SlotsOfValue<VariantOptionValues<TVariants>> | SlotsOfValue<TraitValues<TTraits>>, string>;
|
|
51
|
+
type Result<TSlotKeys extends string = never> = {
|
|
26
52
|
className: string;
|
|
27
53
|
style?: CSSProperties;
|
|
54
|
+
slots?: Partial<Record<TSlotKeys, string>>;
|
|
28
55
|
};
|
|
29
|
-
export declare function windctrl<TVariants extends Record<string, Record<string,
|
|
56
|
+
export declare function windctrl<TVariants extends Record<string, Record<string, SlotAwareValue>> = {}, TTraits extends Record<string, SlotAwareValue> = {}, TDynamic extends Record<string, DynamicResolver> = {}, TScopes extends Record<string, ClassValue> = {}>(config: Config<TVariants, TTraits, TDynamic, TScopes>): (props?: Props<TVariants, TTraits, TDynamic>) => Result<SlotKeys<typeof config.base, TVariants, TTraits>>;
|
|
30
57
|
export declare const wc: typeof windctrl;
|
|
31
58
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,99 @@
|
|
|
1
1
|
import { clsx } from "clsx";
|
|
2
2
|
import { twMerge } from "tailwind-merge";
|
|
3
|
+
function px(prop) {
|
|
4
|
+
return (value) => {
|
|
5
|
+
if (typeof value === "number") {
|
|
6
|
+
return { style: { [prop]: `${value}px` } };
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function num(prop) {
|
|
12
|
+
return (value) => {
|
|
13
|
+
if (typeof value === "number") {
|
|
14
|
+
return { style: { [prop]: value } };
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function opacity() {
|
|
20
|
+
return (value) => {
|
|
21
|
+
if (typeof value === "number") {
|
|
22
|
+
return { style: { opacity: value } };
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function cssVar(name, options) {
|
|
28
|
+
return (value) => {
|
|
29
|
+
if (typeof value === "number") {
|
|
30
|
+
if (options?.unit) {
|
|
31
|
+
return { style: { [name]: `${value}${options.unit}` } };
|
|
32
|
+
}
|
|
33
|
+
return { style: { [name]: String(value) } };
|
|
34
|
+
}
|
|
35
|
+
return { style: { [name]: value } };
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export const dynamic = {
|
|
39
|
+
px,
|
|
40
|
+
num,
|
|
41
|
+
opacity,
|
|
42
|
+
var: cssVar,
|
|
43
|
+
};
|
|
3
44
|
function mergeStyles(...styles) {
|
|
4
45
|
return Object.assign({}, ...styles.filter(Boolean));
|
|
5
46
|
}
|
|
6
|
-
function
|
|
47
|
+
function isSlotAwareValue(value) {
|
|
48
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const obj = value;
|
|
52
|
+
const hasRoot = "root" in obj;
|
|
53
|
+
const hasSlots = "slots" in obj && typeof obj.slots === "object" && obj.slots !== null;
|
|
54
|
+
return hasRoot || hasSlots;
|
|
55
|
+
}
|
|
56
|
+
function addSlotClasses(slotParts, slots) {
|
|
57
|
+
for (const [slotName, slotClasses] of Object.entries(slots)) {
|
|
58
|
+
if (!slotParts[slotName]) {
|
|
59
|
+
slotParts[slotName] = [];
|
|
60
|
+
}
|
|
61
|
+
slotParts[slotName].push(slotClasses);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function processTraits(traits, propsTraits, slotParts) {
|
|
7
65
|
if (!propsTraits)
|
|
8
66
|
return [];
|
|
67
|
+
const rootClasses = [];
|
|
68
|
+
const processTraitKey = (key) => {
|
|
69
|
+
if (!(key in traits))
|
|
70
|
+
return;
|
|
71
|
+
const traitValue = traits[key];
|
|
72
|
+
if (isSlotAwareValue(traitValue)) {
|
|
73
|
+
if (traitValue.root) {
|
|
74
|
+
rootClasses.push(traitValue.root);
|
|
75
|
+
}
|
|
76
|
+
if (traitValue.slots && slotParts) {
|
|
77
|
+
addSlotClasses(slotParts, traitValue.slots);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
rootClasses.push(traitValue);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
9
84
|
if (Array.isArray(propsTraits)) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
85
|
+
for (const key of propsTraits) {
|
|
86
|
+
processTraitKey(key);
|
|
87
|
+
}
|
|
13
88
|
}
|
|
14
|
-
if (typeof propsTraits === "object") {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
89
|
+
else if (typeof propsTraits === "object") {
|
|
90
|
+
for (const [key, value] of Object.entries(propsTraits)) {
|
|
91
|
+
if (value) {
|
|
92
|
+
processTraitKey(key);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
18
95
|
}
|
|
19
|
-
return
|
|
96
|
+
return rootClasses;
|
|
20
97
|
}
|
|
21
98
|
function processDynamicEntries(entries, props) {
|
|
22
99
|
const classNameParts = [];
|
|
@@ -61,23 +138,45 @@ export function windctrl(config) {
|
|
|
61
138
|
return (props = {}) => {
|
|
62
139
|
const classNameParts = [];
|
|
63
140
|
let mergedStyle = {};
|
|
141
|
+
const slotParts = {};
|
|
64
142
|
// Priority order: Base < Variants < Traits < Dynamic
|
|
65
143
|
// (Higher priority classes are added later, so tailwind-merge will keep them)
|
|
66
144
|
// 1. Base classes (lowest priority)
|
|
67
145
|
if (base) {
|
|
68
|
-
|
|
146
|
+
if (isSlotAwareValue(base)) {
|
|
147
|
+
if (base.root) {
|
|
148
|
+
classNameParts.push(base.root);
|
|
149
|
+
}
|
|
150
|
+
if (base.slots) {
|
|
151
|
+
addSlotClasses(slotParts, base.slots);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
classNameParts.push(base);
|
|
156
|
+
}
|
|
69
157
|
}
|
|
70
158
|
// 2. Variants (with defaultVariants fallback)
|
|
71
159
|
for (const [variantKey, variantOptions] of resolvedVariants) {
|
|
72
160
|
const propValue = props[variantKey] ??
|
|
73
161
|
defaultVariants[variantKey];
|
|
74
162
|
if (propValue && variantOptions[propValue]) {
|
|
75
|
-
|
|
163
|
+
const optionValue = variantOptions[propValue];
|
|
164
|
+
if (isSlotAwareValue(optionValue)) {
|
|
165
|
+
if (optionValue.root) {
|
|
166
|
+
classNameParts.push(optionValue.root);
|
|
167
|
+
}
|
|
168
|
+
if (optionValue.slots) {
|
|
169
|
+
addSlotClasses(slotParts, optionValue.slots);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
classNameParts.push(optionValue);
|
|
174
|
+
}
|
|
76
175
|
}
|
|
77
176
|
}
|
|
78
177
|
// 3. Traits (higher priority than variants)
|
|
79
178
|
if (props.traits) {
|
|
80
|
-
classNameParts.push(...processTraits(traits, props.traits));
|
|
179
|
+
classNameParts.push(...processTraits(traits, props.traits, slotParts));
|
|
81
180
|
}
|
|
82
181
|
// 4. Dynamic (highest priority for className)
|
|
83
182
|
if (resolvedDynamicEntries.length) {
|
|
@@ -91,9 +190,27 @@ export function windctrl(config) {
|
|
|
91
190
|
}
|
|
92
191
|
const finalClassName = twMerge(clsx(classNameParts));
|
|
93
192
|
const hasStyle = Object.keys(mergedStyle).length > 0;
|
|
193
|
+
let finalSlots;
|
|
194
|
+
const slotNames = Object.keys(slotParts);
|
|
195
|
+
if (slotNames.length > 0) {
|
|
196
|
+
const out = {};
|
|
197
|
+
for (const slotName of slotNames) {
|
|
198
|
+
const parts = slotParts[slotName];
|
|
199
|
+
if (!parts)
|
|
200
|
+
continue;
|
|
201
|
+
const merged = twMerge(clsx(parts));
|
|
202
|
+
if (merged) {
|
|
203
|
+
out[slotName] = merged;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (Object.keys(out).length > 0) {
|
|
207
|
+
finalSlots = out;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
94
210
|
return {
|
|
95
211
|
className: finalClassName,
|
|
96
212
|
...(hasStyle && { style: mergedStyle }),
|
|
213
|
+
...(finalSlots && { slots: finalSlots }),
|
|
97
214
|
};
|
|
98
215
|
};
|
|
99
216
|
}
|
package/package.json
CHANGED