windctrl 0.2.0 → 0.2.2

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 CHANGED
@@ -257,6 +257,61 @@ const button = windctrl({
257
257
 
258
258
  The scope classes are automatically prefixed with `group-data-[windctrl-scope=...]/windctrl-scope:` to target the parent's data attribute.
259
259
 
260
+ ## Merging External `className` Safely (`wcn()`)
261
+
262
+ WindCtrl resolves Tailwind class conflicts **inside** `windctrl()` using `tailwind-merge`.
263
+ However, in real applications you often need to merge **additional `className` values** at the component boundary.
264
+
265
+ A simple string concat can reintroduce conflicts:
266
+
267
+ ```tsx
268
+ // ⚠️ Can cause subtle Tailwind conflicts (e.g. p-2 vs p-4)
269
+ className={`${result.className} ${className}`}
270
+ ```
271
+
272
+ WindCtrl exports a small helper for this use case:
273
+
274
+ ```tsx
275
+ import { wcn } from "windctrl";
276
+
277
+ // ✅ Conflict-safe merge
278
+ className={wcn(result.className, className)}
279
+ ```
280
+
281
+ `wcn()` is equivalent to `twMerge(clsx(...))` and matches WindCtrl’s internal conflict resolution behavior.
282
+ This keeps the “last one wins” behavior consistent across both generated and user-supplied classes.
283
+
284
+ ## Type Helpers (`StyleProps`)
285
+
286
+ When building reusable components, you often want to expose the exact style-related props inferred from a `windctrl()` definition.
287
+
288
+ WindCtrl exports a small type helper for this purpose:
289
+
290
+ ```typescript
291
+ import type { StyleProps } from "windctrl";
292
+ ```
293
+
294
+ `StyleProps<typeof styles>` extracts all variant, trait, and dynamic props from a WindCtrl instance — similar to `VariantProps` in cva.
295
+
296
+ ```typescript
297
+ const button = windctrl({ ... });
298
+
299
+ type ButtonProps<T extends ElementType = "button"> = {
300
+ as?: T;
301
+ } & Omit<ComponentPropsWithoutRef<T>, keyof StyleProps<typeof button>>
302
+ & StyleProps<typeof button>;
303
+ ```
304
+
305
+ This lets you:
306
+
307
+ - Avoid manually duplicating variant/trait prop definitions
308
+ - Keep component props automatically in sync with styling config
309
+ - Refactor styles without touching component typings
310
+
311
+ > `StyleProps` is optional - you can always define props manually if you prefer.
312
+
313
+ > `wcProps` is provided as an alias of `StyleProps` for convenience.
314
+
260
315
  ## Gotchas
261
316
 
262
317
  - **Tailwind JIT:** Tailwind only generates CSS for class names it can statically detect. Avoid constructing class strings dynamically unless you safelist them.
@@ -0,0 +1,22 @@
1
+ export type CSSProperties = Record<string, string | number>;
2
+ export type DynamicResolverResult = string | {
3
+ className?: string;
4
+ style?: CSSProperties;
5
+ };
6
+ export type DynamicResolver<T = any> = (value: T) => DynamicResolverResult;
7
+ type PxProp = "width" | "height" | "minWidth" | "maxWidth" | "minHeight" | "maxHeight" | "top" | "right" | "bottom" | "left";
8
+ type NumProp = "zIndex" | "flexGrow" | "flexShrink" | "order";
9
+ type VarUnit = "px" | "%" | "deg" | "ms";
10
+ declare function px(prop: PxProp): DynamicResolver<number | string>;
11
+ declare function num(prop: NumProp): DynamicResolver<number | string>;
12
+ declare function opacity(): DynamicResolver<number | string>;
13
+ declare function cssVar(name: `--${string}`, options?: {
14
+ unit?: VarUnit;
15
+ }): DynamicResolver<number | string>;
16
+ export declare const dynamic: {
17
+ px: typeof px;
18
+ num: typeof num;
19
+ opacity: typeof opacity;
20
+ var: typeof cssVar;
21
+ };
22
+ export {};
@@ -0,0 +1,41 @@
1
+ function px(prop) {
2
+ return (value) => {
3
+ if (typeof value === "number") {
4
+ return { style: { [prop]: `${value}px` } };
5
+ }
6
+ return value;
7
+ };
8
+ }
9
+ function num(prop) {
10
+ return (value) => {
11
+ if (typeof value === "number") {
12
+ return { style: { [prop]: value } };
13
+ }
14
+ return value;
15
+ };
16
+ }
17
+ function opacity() {
18
+ return (value) => {
19
+ if (typeof value === "number") {
20
+ return { style: { opacity: value } };
21
+ }
22
+ return value;
23
+ };
24
+ }
25
+ function cssVar(name, options) {
26
+ return (value) => {
27
+ if (typeof value === "number") {
28
+ if (options?.unit) {
29
+ return { style: { [name]: `${value}${options.unit}` } };
30
+ }
31
+ return { style: { [name]: String(value) } };
32
+ }
33
+ return { style: { [name]: value } };
34
+ };
35
+ }
36
+ export const dynamic = {
37
+ px,
38
+ num,
39
+ opacity,
40
+ var: cssVar,
41
+ };
package/dist/index.d.ts CHANGED
@@ -1,25 +1,6 @@
1
1
  import { type ClassValue } from "clsx";
2
- type CSSProperties = Record<string, string | number>;
3
- type DynamicResolverResult = string | {
4
- className?: string;
5
- style?: CSSProperties;
6
- };
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
- };
2
+ import type { CSSProperties, DynamicResolver } from "./dynamic";
3
+ export { dynamic, type CSSProperties, type DynamicResolver } from "./dynamic";
23
4
  type SlotAwareObject = {
24
5
  root?: ClassValue;
25
6
  slots?: Record<string, ClassValue>;
@@ -55,4 +36,6 @@ type Result<TSlotKeys extends string = never> = {
55
36
  };
56
37
  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>>;
57
38
  export declare const wc: typeof windctrl;
58
- export {};
39
+ export declare function wcn(...inputs: ClassValue[]): string;
40
+ export type StyleProps<T> = T extends (props?: infer P) => any ? P : never;
41
+ export type wcProps<T> = StyleProps<T>;
package/dist/index.js CHANGED
@@ -1,46 +1,6 @@
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
+ export { dynamic } from "./dynamic";
44
4
  function mergeStyles(...styles) {
45
5
  return Object.assign({}, ...styles.filter(Boolean));
46
6
  }
@@ -193,15 +153,18 @@ export function windctrl(config) {
193
153
  let finalSlots;
194
154
  const slotNames = Object.keys(slotParts);
195
155
  if (slotNames.length > 0) {
196
- finalSlots = {};
156
+ const out = {};
197
157
  for (const slotName of slotNames) {
198
- const merged = twMerge(clsx(slotParts[slotName]));
158
+ const parts = slotParts[slotName];
159
+ if (!parts)
160
+ continue;
161
+ const merged = twMerge(clsx(parts));
199
162
  if (merged) {
200
- finalSlots[slotName] = merged;
163
+ out[slotName] = merged;
201
164
  }
202
165
  }
203
- if (Object.keys(finalSlots).length === 0) {
204
- finalSlots = undefined;
166
+ if (Object.keys(out).length > 0) {
167
+ finalSlots = out;
205
168
  }
206
169
  }
207
170
  return {
@@ -212,3 +175,6 @@ export function windctrl(config) {
212
175
  };
213
176
  }
214
177
  export const wc = windctrl;
178
+ export function wcn(...inputs) {
179
+ return twMerge(clsx(inputs));
180
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "windctrl",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Advanced variant API for Tailwind CSS with stackable traits and interpolated dynamic styles.",
5
5
  "license": "MIT",
6
6
  "author": "Masaki Morishita (@morishxt)",