diff --git a/src/__tests__/base.test.ts b/src/__tests__/base.test.ts new file mode 100644 index 0000000..c7df095 --- /dev/null +++ b/src/__tests__/base.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from "vitest"; +import { windctrl } from "../"; + +describe("windctrl", () => { + describe("Base classes", () => { + it("should apply base classes when provided", () => { + const button = windctrl({ + base: "rounded px-4 py-2", + }); + + const result = button(); + expect(result.className).toContain("rounded"); + expect(result.className).toContain("px-4"); + expect(result.className).toContain("py-2"); + expect(result.style).toEqual(undefined); + }); + + it("should work without base classes", () => { + const button = windctrl({}); + + const result = button({}); + expect(result.className).toBe(""); + expect(result.style).toEqual(undefined); + }); + }); +}); diff --git a/src/__tests__/dynamic.test.ts b/src/__tests__/dynamic.test.ts new file mode 100644 index 0000000..b5cfd4d --- /dev/null +++ b/src/__tests__/dynamic.test.ts @@ -0,0 +1,235 @@ +import { describe, it, expect } from "vitest"; +import { windctrl, dynamic as d } from "../"; + +describe("windctrl", () => { + describe("Dynamic (Interpolated Variants)", () => { + it("should apply className when dynamic resolver returns string", () => { + const button = windctrl({ + dynamic: { + w: (val) => (typeof val === "number" ? `w-[${val}px]` : `w-${val}`), + }, + }); + + const result = button({ w: "full" }); + expect(result.className).toContain("w-full"); + expect(result.style).toEqual(undefined); + }); + + it("should apply style when dynamic resolver returns object with style", () => { + const button = windctrl({ + dynamic: { + w: (val) => + typeof val === "number" + ? { style: { width: `${val}px` } } + : `w-${val}`, + }, + }); + + const result = button({ w: 200 }); + expect(result.style).toEqual({ width: "200px" }); + }); + + it("should merge className and style when dynamic resolver returns both", () => { + const button = windctrl({ + base: "rounded", + dynamic: { + color: (val) => ({ + className: `text-${val}`, + style: { color: val }, + }), + }, + }); + + const result = button({ color: "red" }); + expect(result.className).toContain("text-red"); + expect(result.style).toEqual({ color: "red" }); + }); + + it("should handle multiple dynamic props", () => { + const button = windctrl({ + dynamic: { + w: (val) => + typeof val === "number" + ? { style: { width: `${val}px` } } + : `w-${val}`, + h: (val) => + typeof val === "number" + ? { style: { height: `${val}px` } } + : `h-${val}`, + }, + }); + + const result = button({ w: 100, h: 200 }); + expect(result.style).toEqual({ width: "100px", height: "200px" }); + }); + + it("should handle mixed dynamic props (string and number)", () => { + const button = windctrl({ + dynamic: { + w: (val) => + typeof val === "number" + ? { style: { width: `${val}px` } } + : `w-${val}`, + }, + }); + + const stringResult = button({ w: "full" }); + expect(stringResult.className).toContain("w-full"); + expect(stringResult.style).toEqual(undefined); + + const numberResult = button({ w: 300 }); + expect(numberResult.style).toEqual({ width: "300px" }); + }); + + it("should resolve style conflicts with last one wins for dynamic styles", () => { + const box = windctrl({ + dynamic: { + w1: () => ({ style: { width: "100px" } }), + w2: () => ({ style: { width: "200px" } }), + }, + }); + + const result = box({ w1: true as any, w2: true as any }); + + expect(result.style).toEqual({ width: "200px" }); + }); + }); + + describe("Dynamic presets", () => { + describe("d.px()", () => { + it("should output inline style for number (px) and keep className empty", () => { + const box = windctrl({ + dynamic: { + w: d.px("width"), + }, + }); + + const result = box({ w: 123 }); + expect(result.style).toEqual({ width: "123px" }); + expect(result.className).toBe(""); + }); + + it("should pass through className string for string input (Unified API)", () => { + const box = windctrl({ + dynamic: { + w: d.px("width"), + }, + }); + + const result = box({ w: "w-full" }); + expect(result.className).toContain("w-full"); + expect(result.style).toEqual(undefined); + }); + }); + + describe("d.num()", () => { + it("should output inline style for number (unitless) and keep className empty", () => { + const layer = windctrl({ + dynamic: { + z: d.num("zIndex"), + }, + }); + + const result = layer({ z: 999 }); + expect(result.style).toEqual({ zIndex: 999 }); + expect(result.className).toBe(""); + }); + + it("should pass through className string for string input (Unified API)", () => { + const layer = windctrl({ + dynamic: { + z: d.num("zIndex"), + }, + }); + + const result = layer({ z: "z-50" }); + expect(result.className).toContain("z-50"); + expect(result.style).toEqual(undefined); + }); + }); + + describe("d.opacity()", () => { + it("should output inline style for number and keep className empty", () => { + const fade = windctrl({ + dynamic: { + opacity: d.opacity(), + }, + }); + + const result = fade({ opacity: 0.4 }); + expect(result.style).toEqual({ opacity: 0.4 }); + expect(result.className).toBe(""); + }); + + it("should pass through className string for string input (Unified API)", () => { + const fade = windctrl({ + dynamic: { + opacity: d.opacity(), + }, + }); + + const result = fade({ opacity: "opacity-50" }); + expect(result.className).toContain("opacity-50"); + expect(result.style).toEqual(undefined); + }); + }); + + describe("d.var()", () => { + it("should set CSS variable as inline style for number with unit (no className output)", () => { + const card = windctrl({ + dynamic: { + x: d.var("--x", { unit: "px" }), + }, + }); + + const result = card({ x: 12 }); + + // NOTE: CSS custom properties are stored as object keys. + expect(result.style).toEqual({ "--x": "12px" }); + expect(result.className).toBe(""); + }); + + it("should set CSS variable as inline style for string value (no className output)", () => { + const card = windctrl({ + dynamic: { + x: d.var("--x"), + }, + }); + + const result = card({ x: "10%" }); + expect(result.style).toEqual({ "--x": "10%" }); + expect(result.className).toBe(""); + }); + + it("should merge multiple CSS variables via last-one-wins when same variable is set twice", () => { + const card = windctrl({ + dynamic: { + x1: d.var("--x", { unit: "px" }), + x2: d.var("--x", { unit: "px" }), + }, + }); + + const result = card({ x1: 10, x2: 20 }); + + // last one wins + expect(result.style).toEqual({ "--x": "20px" }); + }); + }); + + it("should coexist with other dynamic resolvers (className + style merge)", () => { + const box = windctrl({ + dynamic: { + w: d.px("width"), + opacity: d.opacity(), + // keep an existing custom resolver in the same config + custom: (v) => (v ? "ring-2" : ""), + }, + }); + + const result = box({ w: 100, opacity: 0.5, custom: true }); + + expect(result.style).toEqual({ width: "100px", opacity: 0.5 }); + expect(result.className).toContain("ring-2"); + }); + }); +}); diff --git a/src/__tests__/integration.test.ts b/src/__tests__/integration.test.ts new file mode 100644 index 0000000..284b1f7 --- /dev/null +++ b/src/__tests__/integration.test.ts @@ -0,0 +1,237 @@ +import { describe, it, expect } from "vitest"; +import { windctrl } from "../"; + +describe("windctrl", () => { + describe("Priority and Integration", () => { + it("should apply classes in correct priority: Dynamic > Traits > Variants > Base", () => { + const button = windctrl({ + base: "base-class", + variants: { + intent: { + primary: "variant-class", + }, + }, + traits: { + loading: "trait-class", + }, + dynamic: { + w: (val) => `dynamic-class-${val}`, + }, + }); + + const result = button({ + intent: "primary", + traits: ["loading"], + w: "test", + }); + + expect(result.className).toContain("base-class"); + expect(result.className).toContain("variant-class"); + expect(result.className).toContain("trait-class"); + expect(result.className).toContain("dynamic-class-test"); + }); + + it("should handle complex real-world scenario", () => { + const button = windctrl({ + base: "rounded px-4 py-2 font-medium transition", + variants: { + intent: { + primary: "bg-blue-500 text-white hover:bg-blue-600", + secondary: "bg-gray-500 text-gray-900 hover:bg-gray-600", + }, + size: { + sm: "text-sm", + md: "text-base", + lg: "text-lg", + }, + }, + traits: { + loading: "opacity-50 cursor-wait", + glass: "backdrop-blur bg-white/10", + disabled: "pointer-events-none opacity-50", + }, + dynamic: { + w: (val) => + typeof val === "number" + ? { style: { width: `${val}px` } } + : `w-${val}`, + }, + defaultVariants: { + intent: "primary", + size: "md", + }, + scopes: { + header: "text-sm", + }, + }); + + const result = button({ + intent: "secondary", + size: "lg", + traits: ["loading", "glass"], + w: 200, + }); + + expect(result.className).toContain("rounded"); + expect(result.className).toContain("px-4"); + expect(result.className).toContain("py-2"); + + expect(result.className).toContain("text-gray-900"); + expect(result.className).toContain("hover:bg-gray-600"); + expect(result.className).toContain("text-lg"); + + expect(result.className).toContain("opacity-50"); + expect(result.className).toContain("backdrop-blur"); + + expect(result.style).toEqual({ width: "200px" }); + + expect(result.className).toContain( + "group-data-[windctrl-scope=header]/windctrl-scope:text-sm", + ); + }); + + it("should merge conflicting Tailwind classes (last one wins)", () => { + const button = windctrl({ + base: "text-red-500", + variants: { + intent: { + primary: "text-blue-500", + }, + }, + }); + + const result = button({ intent: "primary" }); + expect(result.className).toContain("text-blue-500"); + expect(result.className).not.toContain("text-red-500"); + }); + + it("should let Dynamic override Traits when Tailwind classes conflict (via twMerge)", () => { + const button = windctrl({ + traits: { + padded: "p-2", + }, + dynamic: { + p: (val) => (typeof val === "number" ? `p-${val}` : val), + }, + }); + + const result = button({ traits: ["padded"], p: 4 }); + + expect(result.className).toContain("p-4"); + expect(result.className).not.toContain("p-2"); + }); + + it("should let Traits override Base when Tailwind classes conflict (via twMerge)", () => { + const button = windctrl({ + base: "p-1", + traits: { padded: "p-3" }, + }); + + const result = button({ traits: ["padded"] }); + + expect(result.className).toContain("p-3"); + expect(result.className).not.toContain("p-1"); + }); + }); + + describe("Edge cases", () => { + it("should handle empty configuration", () => { + const button = windctrl({}); + const result = button({}); + expect(result.className).toBe(""); + expect(result.style).toEqual(undefined); + }); + + it("should handle undefined props gracefully", () => { + const button = windctrl({ + variants: { + intent: { + primary: "bg-blue-500", + }, + }, + }); + + const result = button({ intent: undefined as any }); + expect(result.className).not.toContain("bg-blue-500"); + }); + + it("should handle null props gracefully", () => { + const button = windctrl({ + variants: { + intent: { + primary: "bg-blue-500", + }, + }, + }); + + const result = button({ intent: null as any }); + expect(result.className).not.toContain("bg-blue-500"); + }); + + it("should handle traits with invalid keys gracefully", () => { + const button = windctrl({ + traits: { + loading: "opacity-50", + }, + }); + + const result = button({ traits: ["invalid-trait" as any] }); + expect(result.className).not.toContain("opacity-50"); + }); + }); + + describe("Type safety", () => { + it("should infer variant prop types correctly", () => { + const button = windctrl({ + variants: { + intent: { + primary: "bg-blue-500", + secondary: "bg-gray-500", + }, + }, + }); + + const result1 = button({ intent: "primary" }); + expect(result1.className).toContain("bg-blue-500"); + + const result2 = button({ intent: "secondary" }); + expect(result2.className).toContain("bg-gray-500"); + }); + + it("should infer trait keys correctly", () => { + const button = windctrl({ + traits: { + loading: "opacity-50", + glass: "backdrop-blur", + }, + }); + + const result1 = button({ traits: ["loading", "glass"] }); + expect(result1.className).toContain("opacity-50"); + expect(result1.className).toContain("backdrop-blur"); + + const result2 = button({ + traits: { loading: true, glass: false }, + }); + expect(result2.className).toContain("opacity-50"); + expect(result2.className).not.toContain("backdrop-blur"); + }); + + it("should infer dynamic prop types correctly", () => { + const button = windctrl({ + dynamic: { + w: (val) => + typeof val === "number" + ? { style: { width: `${val}px` } } + : `w-${val}`, + }, + }); + + const result1 = button({ w: "full" }); + expect(result1.className).toContain("w-full"); + + const result2 = button({ w: 100 }); + expect(result2.style).toEqual({ width: "100px" }); + }); + }); +}); diff --git a/src/__tests__/scopes.test.ts b/src/__tests__/scopes.test.ts new file mode 100644 index 0000000..fe78cb2 --- /dev/null +++ b/src/__tests__/scopes.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from "vitest"; +import { windctrl } from "../"; + +describe("windctrl", () => { + describe("Scopes", () => { + it("should apply scope classes with group-data selector", () => { + const button = windctrl({ + base: "rounded", + scopes: { + header: "text-sm", + footer: "text-xs", + }, + }); + + const result = button({}); + expect(result.className).toContain( + "group-data-[windctrl-scope=header]/windctrl-scope:text-sm", + ); + expect(result.className).toContain( + "group-data-[windctrl-scope=footer]/windctrl-scope:text-xs", + ); + }); + + it("should combine scopes with base classes", () => { + const button = windctrl({ + base: "px-4 py-2", + scopes: { + header: "text-sm", + }, + }); + + const result = button({}); + expect(result.className).toContain("px-4"); + expect(result.className).toContain("py-2"); + expect(result.className).toContain( + "group-data-[windctrl-scope=header]/windctrl-scope:text-sm", + ); + }); + + it("should prefix every scope class when multiple classes are provided", () => { + const button = windctrl({ + scopes: { + header: "text-sm py-1", + }, + }); + + const result = button({}); + + expect(result.className).toContain( + "group-data-[windctrl-scope=header]/windctrl-scope:text-sm", + ); + expect(result.className).toContain( + "group-data-[windctrl-scope=header]/windctrl-scope:py-1", + ); + }); + }); +}); diff --git a/src/__tests__/slots.test.ts b/src/__tests__/slots.test.ts new file mode 100644 index 0000000..d25fc86 --- /dev/null +++ b/src/__tests__/slots.test.ts @@ -0,0 +1,144 @@ +import { describe, it, expect } from "vitest"; +import { windctrl } from "../"; + +describe("windctrl", () => { + describe("Slots", () => { + it("should return slots as class strings and keep root as className/style", () => { + const button = windctrl({ + base: { + root: "rounded", + slots: { + icon: "shrink-0", + label: "truncate", + }, + }, + }); + + const result = button(); + + // root stays on className/style + expect(result.className).toContain("rounded"); + expect(result.style).toEqual(undefined); + + // slots exist as strings + expect(result.slots?.icon).toContain("shrink-0"); + expect(result.slots?.label).toContain("truncate"); + }); + + it("should apply variant slot classes based on prop value (and keep root variants working)", () => { + const button = windctrl({ + base: { + root: "inline-flex", + slots: { icon: "shrink-0" }, + }, + variants: { + size: { + sm: { + root: "h-8", + slots: { icon: "h-3 w-3" }, + }, + md: { + root: "h-10", + slots: { icon: "h-4 w-4" }, + }, + }, + }, + defaultVariants: { size: "md" }, + }); + + const sm = button({ size: "sm" }); + expect(sm.className).toContain("inline-flex"); + expect(sm.className).toContain("h-8"); + expect(sm.slots?.icon).toContain("h-3"); + expect(sm.slots?.icon).toContain("w-3"); + + const fallback = button({}); + expect(fallback.className).toContain("h-10"); + expect(fallback.slots?.icon).toContain("h-4"); + expect(fallback.slots?.icon).toContain("w-4"); + }); + + it("should apply trait slot classes (array form) and merge with base/variants", () => { + const button = windctrl({ + base: { + root: "inline-flex", + slots: { icon: "shrink-0" }, + }, + variants: { + size: { + sm: { slots: { icon: "h-3 w-3" } }, + }, + }, + traits: { + loading: { + root: "opacity-70", + slots: { icon: "animate-spin" }, + }, + }, + }); + + const result = button({ size: "sm", traits: ["loading"] }); + + // root gets trait too + expect(result.className).toContain("opacity-70"); + + // icon gets base + variant + trait + expect(result.slots?.icon).toContain("shrink-0"); + expect(result.slots?.icon).toContain("h-3"); + expect(result.slots?.icon).toContain("w-3"); + expect(result.slots?.icon).toContain("animate-spin"); + }); + + it("should let Traits override Variants on slots when Tailwind classes conflict (via twMerge)", () => { + const button = windctrl({ + variants: { + intent: { + primary: { slots: { icon: "text-blue-500" } }, + }, + }, + traits: { + dangerIcon: { slots: { icon: "text-red-500" } }, + }, + }); + + const result = button({ intent: "primary", traits: ["dangerIcon"] }); + + // last one wins: Traits > Variants + expect(result.slots?.icon).toContain("text-red-500"); + expect(result.slots?.icon).not.toContain("text-blue-500"); + }); + + it("should ignore invalid trait keys for slots gracefully (same behavior as root traits)", () => { + const button = windctrl({ + traits: { + loading: { slots: { icon: "animate-spin" } }, + }, + }); + + const result = button({ traits: ["invalid-trait" as any] }); + + expect(result.slots?.icon).toBe(undefined); + }); + + it("should not include slots when slots are not configured", () => { + const button = windctrl({ + base: "rounded", + variants: { + size: { + sm: "text-sm", + }, + }, + traits: { + loading: "opacity-50", + }, + }); + + const result = button({ size: "sm", traits: ["loading"] }); + + expect(result.className).toContain("rounded"); + expect(result.className).toContain("text-sm"); + expect(result.className).toContain("opacity-50"); + expect((result as any).slots).toBe(undefined); + }); + }); +}); diff --git a/src/__tests__/traits.test.ts b/src/__tests__/traits.test.ts new file mode 100644 index 0000000..912ba35 --- /dev/null +++ b/src/__tests__/traits.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect } from "vitest"; +import { windctrl } from "../"; + +describe("windctrl", () => { + describe("Traits", () => { + it("should apply trait classes when provided as array", () => { + const button = windctrl({ + base: "rounded", + traits: { + loading: "opacity-50 cursor-wait", + glass: "backdrop-blur bg-white/10", + }, + }); + + const result = button({ traits: ["loading", "glass"] }); + expect(result.className).toContain("opacity-50"); + expect(result.className).toContain("cursor-wait"); + expect(result.className).toContain("backdrop-blur"); + expect(result.className).toContain("bg-white/10"); + expect(result.className).toContain("rounded"); + }); + + it("should apply trait classes when provided as object", () => { + const button = windctrl({ + traits: { + loading: "opacity-50", + glass: "backdrop-blur", + disabled: "pointer-events-none", + }, + }); + + const result = button({ + traits: { loading: true, glass: true, disabled: false }, + }); + expect(result.className).toContain("opacity-50"); + expect(result.className).toContain("backdrop-blur"); + expect(result.className).not.toContain("pointer-events-none"); + }); + + it("should handle empty traits array", () => { + const button = windctrl({ + base: "rounded", + traits: { + loading: "opacity-50", + }, + }); + + const result = button({ traits: [] }); + expect(result.className).toContain("rounded"); + expect(result.className).not.toContain("opacity-50"); + }); + + it("should handle empty traits object", () => { + const button = windctrl({ + base: "rounded", + traits: { + loading: "opacity-50", + }, + }); + + const result = button({ traits: {} }); + expect(result.className).toContain("rounded"); + expect(result.className).not.toContain("opacity-50"); + }); + + it("should apply multiple traits orthogonally", () => { + const button = windctrl({ + traits: { + loading: "opacity-50", + glass: "backdrop-blur", + error: "border-red-500", + }, + }); + + const result = button({ + traits: ["loading", "glass", "error"], + }); + expect(result.className).toContain("opacity-50"); + expect(result.className).toContain("backdrop-blur"); + expect(result.className).toContain("border-red-500"); + }); + }); +}); diff --git a/src/__tests__/variants.test.ts b/src/__tests__/variants.test.ts new file mode 100644 index 0000000..41fde2a --- /dev/null +++ b/src/__tests__/variants.test.ts @@ -0,0 +1,123 @@ +import { describe, it, expect } from "vitest"; +import { windctrl } from "../"; + +describe("windctrl", () => { + describe("Variants", () => { + it("should apply variant classes based on prop value", () => { + const button = windctrl({ + base: "rounded", + variants: { + intent: { + primary: "bg-blue-500 text-white", + secondary: "bg-gray-500 text-gray-900", + }, + }, + }); + + const primaryResult = button({ intent: "primary" }); + expect(primaryResult.className).toContain("bg-blue-500"); + expect(primaryResult.className).toContain("text-white"); + expect(primaryResult.className).toContain("rounded"); + + const secondaryResult = button({ intent: "secondary" }); + expect(secondaryResult.className).toContain("bg-gray-500"); + expect(secondaryResult.className).toContain("text-gray-900"); + expect(secondaryResult.className).toContain("rounded"); + }); + + it("should handle multiple variant dimensions", () => { + const button = windctrl({ + variants: { + size: { + sm: "text-sm", + md: "text-base", + lg: "text-lg", + }, + intent: { + primary: "bg-blue-500", + secondary: "bg-gray-500", + }, + }, + }); + + const result = button({ size: "sm", intent: "primary" }); + expect(result.className).toContain("text-sm"); + expect(result.className).toContain("bg-blue-500"); + }); + + it("should not apply variant classes when prop is not provided", () => { + const button = windctrl({ + variants: { + intent: { + primary: "bg-blue-500", + secondary: "bg-gray-500", + }, + }, + }); + + const result = button({}); + expect(result.className).not.toContain("bg-blue-500"); + expect(result.className).not.toContain("bg-gray-500"); + }); + }); + + describe("Default variants", () => { + it("should apply default variant values when prop is not provided", () => { + const button = windctrl({ + variants: { + intent: { + primary: "bg-blue-500", + secondary: "bg-gray-500", + }, + }, + defaultVariants: { + intent: "primary", + }, + }); + + const result = button({}); + expect(result.className).toContain("bg-blue-500"); + }); + + it("should allow overriding default variants", () => { + const button = windctrl({ + variants: { + intent: { + primary: "bg-blue-500", + secondary: "bg-gray-500", + }, + }, + defaultVariants: { + intent: "primary", + }, + }); + + const result = button({ intent: "secondary" }); + expect(result.className).toContain("bg-gray-500"); + expect(result.className).not.toContain("bg-blue-500"); + }); + + it("should handle multiple default variants", () => { + const button = windctrl({ + variants: { + size: { + sm: "text-sm", + md: "text-base", + }, + intent: { + primary: "bg-blue-500", + secondary: "bg-gray-500", + }, + }, + defaultVariants: { + size: "md", + intent: "primary", + }, + }); + + const result = button({}); + expect(result.className).toContain("text-base"); + expect(result.className).toContain("bg-blue-500"); + }); + }); +}); diff --git a/src/__tests__/wc-alias.test.ts b/src/__tests__/wc-alias.test.ts new file mode 100644 index 0000000..e7cd469 --- /dev/null +++ b/src/__tests__/wc-alias.test.ts @@ -0,0 +1,8 @@ +import { describe, it, expect } from "vitest"; +import { windctrl, wc } from "../"; + +describe("wc", () => { + it("should be the same as windctrl", () => { + expect(wc).toBe(windctrl); + }); +}); diff --git a/src/__tests__/wcn.test.ts b/src/__tests__/wcn.test.ts new file mode 100644 index 0000000..e45a453 --- /dev/null +++ b/src/__tests__/wcn.test.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from "vitest"; +import { wcn } from "../"; + +describe("wcn", () => { + it("should merge class names with clsx behavior", () => { + expect(wcn("a", false && "b", null as any, undefined, "c")).toBe("a c"); + }); + + it("should resolve Tailwind conflicts using tailwind-merge (last one wins)", () => { + expect(wcn("p-2", "p-4")).toBe("p-4"); + }); + + it("should handle arrays and objects like clsx", () => { + expect(wcn(["a", ["b"]], { c: true, d: false })).toBe("a b c"); + }); +}); diff --git a/src/dynamic.ts b/src/dynamic.ts new file mode 100644 index 0000000..5d7d9ea --- /dev/null +++ b/src/dynamic.ts @@ -0,0 +1,72 @@ +export type CSSProperties = Record; + +export type DynamicResolverResult = + | string + | { className?: string; style?: CSSProperties }; + +export type DynamicResolver = (value: T) => DynamicResolverResult; + +type PxProp = + | "width" + | "height" + | "minWidth" + | "maxWidth" + | "minHeight" + | "maxHeight" + | "top" + | "right" + | "bottom" + | "left"; + +type NumProp = "zIndex" | "flexGrow" | "flexShrink" | "order"; + +type VarUnit = "px" | "%" | "deg" | "ms"; + +function px(prop: PxProp): DynamicResolver { + return (value: number | string): DynamicResolverResult => { + if (typeof value === "number") { + return { style: { [prop]: `${value}px` } }; + } + return value; + }; +} + +function num(prop: NumProp): DynamicResolver { + return (value: number | string): DynamicResolverResult => { + if (typeof value === "number") { + return { style: { [prop]: value } }; + } + return value; + }; +} + +function opacity(): DynamicResolver { + return (value: number | string): DynamicResolverResult => { + if (typeof value === "number") { + return { style: { opacity: value } }; + } + return value; + }; +} + +function cssVar( + name: `--${string}`, + options?: { unit?: VarUnit }, +): DynamicResolver { + return (value: number | string): DynamicResolverResult => { + if (typeof value === "number") { + if (options?.unit) { + return { style: { [name]: `${value}${options.unit}` } }; + } + return { style: { [name]: String(value) } }; + } + return { style: { [name]: value } }; + }; +} + +export const dynamic = { + px, + num, + opacity, + var: cssVar, +}; diff --git a/src/index.test.ts b/src/index.test.ts deleted file mode 100644 index ff0dfc9..0000000 --- a/src/index.test.ts +++ /dev/null @@ -1,901 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { windctrl, wc, dynamic as d, wcn } from "./"; - -describe("wc", () => { - it("should be the same as windctrl", () => { - expect(wc).toBe(windctrl); - }); -}); - -describe("wcn", () => { - it("should merge class names with clsx behavior", () => { - expect(wcn("a", false && "b", null as any, undefined, "c")).toBe("a c"); - }); - - it("should resolve Tailwind conflicts using tailwind-merge (last one wins)", () => { - expect(wcn("p-2", "p-4")).toBe("p-4"); - }); - - it("should handle arrays and objects like clsx", () => { - expect(wcn(["a", ["b"]], { c: true, d: false })).toBe("a b c"); - }); -}); - -describe("windctrl", () => { - describe("Base classes", () => { - it("should apply base classes when provided", () => { - const button = windctrl({ - base: "rounded px-4 py-2", - }); - - const result = button(); - expect(result.className).toContain("rounded"); - expect(result.className).toContain("px-4"); - expect(result.className).toContain("py-2"); - expect(result.style).toEqual(undefined); - }); - - it("should work without base classes", () => { - const button = windctrl({}); - - const result = button({}); - expect(result.className).toBe(""); - expect(result.style).toEqual(undefined); - }); - }); - - describe("Variants", () => { - it("should apply variant classes based on prop value", () => { - const button = windctrl({ - base: "rounded", - variants: { - intent: { - primary: "bg-blue-500 text-white", - secondary: "bg-gray-500 text-gray-900", - }, - }, - }); - - const primaryResult = button({ intent: "primary" }); - expect(primaryResult.className).toContain("bg-blue-500"); - expect(primaryResult.className).toContain("text-white"); - expect(primaryResult.className).toContain("rounded"); - - const secondaryResult = button({ intent: "secondary" }); - expect(secondaryResult.className).toContain("bg-gray-500"); - expect(secondaryResult.className).toContain("text-gray-900"); - expect(secondaryResult.className).toContain("rounded"); - }); - - it("should handle multiple variant dimensions", () => { - const button = windctrl({ - variants: { - size: { - sm: "text-sm", - md: "text-base", - lg: "text-lg", - }, - intent: { - primary: "bg-blue-500", - secondary: "bg-gray-500", - }, - }, - }); - - const result = button({ size: "sm", intent: "primary" }); - expect(result.className).toContain("text-sm"); - expect(result.className).toContain("bg-blue-500"); - }); - - it("should not apply variant classes when prop is not provided", () => { - const button = windctrl({ - variants: { - intent: { - primary: "bg-blue-500", - secondary: "bg-gray-500", - }, - }, - }); - - const result = button({}); - expect(result.className).not.toContain("bg-blue-500"); - expect(result.className).not.toContain("bg-gray-500"); - }); - }); - - describe("Default variants", () => { - it("should apply default variant values when prop is not provided", () => { - const button = windctrl({ - variants: { - intent: { - primary: "bg-blue-500", - secondary: "bg-gray-500", - }, - }, - defaultVariants: { - intent: "primary", - }, - }); - - const result = button({}); - expect(result.className).toContain("bg-blue-500"); - }); - - it("should allow overriding default variants", () => { - const button = windctrl({ - variants: { - intent: { - primary: "bg-blue-500", - secondary: "bg-gray-500", - }, - }, - defaultVariants: { - intent: "primary", - }, - }); - - const result = button({ intent: "secondary" }); - expect(result.className).toContain("bg-gray-500"); - expect(result.className).not.toContain("bg-blue-500"); - }); - - it("should handle multiple default variants", () => { - const button = windctrl({ - variants: { - size: { - sm: "text-sm", - md: "text-base", - }, - intent: { - primary: "bg-blue-500", - secondary: "bg-gray-500", - }, - }, - defaultVariants: { - size: "md", - intent: "primary", - }, - }); - - const result = button({}); - expect(result.className).toContain("text-base"); - expect(result.className).toContain("bg-blue-500"); - }); - }); - - describe("Traits", () => { - it("should apply trait classes when provided as array", () => { - const button = windctrl({ - base: "rounded", - traits: { - loading: "opacity-50 cursor-wait", - glass: "backdrop-blur bg-white/10", - }, - }); - - const result = button({ traits: ["loading", "glass"] }); - expect(result.className).toContain("opacity-50"); - expect(result.className).toContain("cursor-wait"); - expect(result.className).toContain("backdrop-blur"); - expect(result.className).toContain("bg-white/10"); - expect(result.className).toContain("rounded"); - }); - - it("should apply trait classes when provided as object", () => { - const button = windctrl({ - traits: { - loading: "opacity-50", - glass: "backdrop-blur", - disabled: "pointer-events-none", - }, - }); - - const result = button({ - traits: { loading: true, glass: true, disabled: false }, - }); - expect(result.className).toContain("opacity-50"); - expect(result.className).toContain("backdrop-blur"); - expect(result.className).not.toContain("pointer-events-none"); - }); - - it("should handle empty traits array", () => { - const button = windctrl({ - base: "rounded", - traits: { - loading: "opacity-50", - }, - }); - - const result = button({ traits: [] }); - expect(result.className).toContain("rounded"); - expect(result.className).not.toContain("opacity-50"); - }); - - it("should handle empty traits object", () => { - const button = windctrl({ - base: "rounded", - traits: { - loading: "opacity-50", - }, - }); - - const result = button({ traits: {} }); - expect(result.className).toContain("rounded"); - expect(result.className).not.toContain("opacity-50"); - }); - - it("should apply multiple traits orthogonally", () => { - const button = windctrl({ - traits: { - loading: "opacity-50", - glass: "backdrop-blur", - error: "border-red-500", - }, - }); - - const result = button({ - traits: ["loading", "glass", "error"], - }); - expect(result.className).toContain("opacity-50"); - expect(result.className).toContain("backdrop-blur"); - expect(result.className).toContain("border-red-500"); - }); - }); - - describe("Dynamic (Interpolated Variants)", () => { - it("should apply className when dynamic resolver returns string", () => { - const button = windctrl({ - dynamic: { - w: (val) => (typeof val === "number" ? `w-[${val}px]` : `w-${val}`), - }, - }); - - const result = button({ w: "full" }); - expect(result.className).toContain("w-full"); - expect(result.style).toEqual(undefined); - }); - - it("should apply style when dynamic resolver returns object with style", () => { - const button = windctrl({ - dynamic: { - w: (val) => - typeof val === "number" - ? { style: { width: `${val}px` } } - : `w-${val}`, - }, - }); - - const result = button({ w: 200 }); - expect(result.style).toEqual({ width: "200px" }); - }); - - it("should merge className and style when dynamic resolver returns both", () => { - const button = windctrl({ - base: "rounded", - dynamic: { - color: (val) => ({ - className: `text-${val}`, - style: { color: val }, - }), - }, - }); - - const result = button({ color: "red" }); - expect(result.className).toContain("text-red"); - expect(result.style).toEqual({ color: "red" }); - }); - - it("should handle multiple dynamic props", () => { - const button = windctrl({ - dynamic: { - w: (val) => - typeof val === "number" - ? { style: { width: `${val}px` } } - : `w-${val}`, - h: (val) => - typeof val === "number" - ? { style: { height: `${val}px` } } - : `h-${val}`, - }, - }); - - const result = button({ w: 100, h: 200 }); - expect(result.style).toEqual({ width: "100px", height: "200px" }); - }); - - it("should handle mixed dynamic props (string and number)", () => { - const button = windctrl({ - dynamic: { - w: (val) => - typeof val === "number" - ? { style: { width: `${val}px` } } - : `w-${val}`, - }, - }); - - const stringResult = button({ w: "full" }); - expect(stringResult.className).toContain("w-full"); - expect(stringResult.style).toEqual(undefined); - - const numberResult = button({ w: 300 }); - expect(numberResult.style).toEqual({ width: "300px" }); - }); - - it("should resolve style conflicts with last one wins for dynamic styles", () => { - const box = windctrl({ - dynamic: { - w1: () => ({ style: { width: "100px" } }), - w2: () => ({ style: { width: "200px" } }), - }, - }); - - const result = box({ w1: true as any, w2: true as any }); - - expect(result.style).toEqual({ width: "200px" }); - }); - }); - - describe("Dynamic presets", () => { - describe("d.px()", () => { - it("should output inline style for number (px) and keep className empty", () => { - const box = windctrl({ - dynamic: { - w: d.px("width"), - }, - }); - - const result = box({ w: 123 }); - expect(result.style).toEqual({ width: "123px" }); - expect(result.className).toBe(""); - }); - - it("should pass through className string for string input (Unified API)", () => { - const box = windctrl({ - dynamic: { - w: d.px("width"), - }, - }); - - const result = box({ w: "w-full" }); - expect(result.className).toContain("w-full"); - expect(result.style).toEqual(undefined); - }); - }); - - describe("d.num()", () => { - it("should output inline style for number (unitless) and keep className empty", () => { - const layer = windctrl({ - dynamic: { - z: d.num("zIndex"), - }, - }); - - const result = layer({ z: 999 }); - expect(result.style).toEqual({ zIndex: 999 }); - expect(result.className).toBe(""); - }); - - it("should pass through className string for string input (Unified API)", () => { - const layer = windctrl({ - dynamic: { - z: d.num("zIndex"), - }, - }); - - const result = layer({ z: "z-50" }); - expect(result.className).toContain("z-50"); - expect(result.style).toEqual(undefined); - }); - }); - - describe("d.opacity()", () => { - it("should output inline style for number and keep className empty", () => { - const fade = windctrl({ - dynamic: { - opacity: d.opacity(), - }, - }); - - const result = fade({ opacity: 0.4 }); - expect(result.style).toEqual({ opacity: 0.4 }); - expect(result.className).toBe(""); - }); - - it("should pass through className string for string input (Unified API)", () => { - const fade = windctrl({ - dynamic: { - opacity: d.opacity(), - }, - }); - - const result = fade({ opacity: "opacity-50" }); - expect(result.className).toContain("opacity-50"); - expect(result.style).toEqual(undefined); - }); - }); - - describe("d.var()", () => { - it("should set CSS variable as inline style for number with unit (no className output)", () => { - const card = windctrl({ - dynamic: { - x: d.var("--x", { unit: "px" }), - }, - }); - - const result = card({ x: 12 }); - - // NOTE: CSS custom properties are stored as object keys. - expect(result.style).toEqual({ "--x": "12px" }); - expect(result.className).toBe(""); - }); - - it("should set CSS variable as inline style for string value (no className output)", () => { - const card = windctrl({ - dynamic: { - x: d.var("--x"), - }, - }); - - const result = card({ x: "10%" }); - expect(result.style).toEqual({ "--x": "10%" }); - expect(result.className).toBe(""); - }); - - it("should merge multiple CSS variables via last-one-wins when same variable is set twice", () => { - const card = windctrl({ - dynamic: { - x1: d.var("--x", { unit: "px" }), - x2: d.var("--x", { unit: "px" }), - }, - }); - - const result = card({ x1: 10, x2: 20 }); - - // last one wins - expect(result.style).toEqual({ "--x": "20px" }); - }); - }); - - it("should coexist with other dynamic resolvers (className + style merge)", () => { - const box = windctrl({ - dynamic: { - w: d.px("width"), - opacity: d.opacity(), - // keep an existing custom resolver in the same config - custom: (v) => (v ? "ring-2" : ""), - }, - }); - - const result = box({ w: 100, opacity: 0.5, custom: true }); - - expect(result.style).toEqual({ width: "100px", opacity: 0.5 }); - expect(result.className).toContain("ring-2"); - }); - }); - - describe("Scopes", () => { - it("should apply scope classes with group-data selector", () => { - const button = windctrl({ - base: "rounded", - scopes: { - header: "text-sm", - footer: "text-xs", - }, - }); - - const result = button({}); - expect(result.className).toContain( - "group-data-[windctrl-scope=header]/windctrl-scope:text-sm", - ); - expect(result.className).toContain( - "group-data-[windctrl-scope=footer]/windctrl-scope:text-xs", - ); - }); - - it("should combine scopes with base classes", () => { - const button = windctrl({ - base: "px-4 py-2", - scopes: { - header: "text-sm", - }, - }); - - const result = button({}); - expect(result.className).toContain("px-4"); - expect(result.className).toContain("py-2"); - expect(result.className).toContain( - "group-data-[windctrl-scope=header]/windctrl-scope:text-sm", - ); - }); - - it("should prefix every scope class when multiple classes are provided", () => { - const button = windctrl({ - scopes: { - header: "text-sm py-1", - }, - }); - - const result = button({}); - - expect(result.className).toContain( - "group-data-[windctrl-scope=header]/windctrl-scope:text-sm", - ); - expect(result.className).toContain( - "group-data-[windctrl-scope=header]/windctrl-scope:py-1", - ); - }); - }); - - describe("Priority and Integration", () => { - it("should apply classes in correct priority: Dynamic > Traits > Variants > Base", () => { - const button = windctrl({ - base: "base-class", - variants: { - intent: { - primary: "variant-class", - }, - }, - traits: { - loading: "trait-class", - }, - dynamic: { - w: (val) => `dynamic-class-${val}`, - }, - }); - - const result = button({ - intent: "primary", - traits: ["loading"], - w: "test", - }); - - expect(result.className).toContain("base-class"); - expect(result.className).toContain("variant-class"); - expect(result.className).toContain("trait-class"); - expect(result.className).toContain("dynamic-class-test"); - }); - - it("should handle complex real-world scenario", () => { - const button = windctrl({ - base: "rounded px-4 py-2 font-medium transition", - variants: { - intent: { - primary: "bg-blue-500 text-white hover:bg-blue-600", - secondary: "bg-gray-500 text-gray-900 hover:bg-gray-600", - }, - size: { - sm: "text-sm", - md: "text-base", - lg: "text-lg", - }, - }, - traits: { - loading: "opacity-50 cursor-wait", - glass: "backdrop-blur bg-white/10", - disabled: "pointer-events-none opacity-50", - }, - dynamic: { - w: (val) => - typeof val === "number" - ? { style: { width: `${val}px` } } - : `w-${val}`, - }, - defaultVariants: { - intent: "primary", - size: "md", - }, - scopes: { - header: "text-sm", - }, - }); - - const result = button({ - intent: "secondary", - size: "lg", - traits: ["loading", "glass"], - w: 200, - }); - - expect(result.className).toContain("rounded"); - expect(result.className).toContain("px-4"); - expect(result.className).toContain("py-2"); - - expect(result.className).toContain("text-gray-900"); - expect(result.className).toContain("hover:bg-gray-600"); - expect(result.className).toContain("text-lg"); - - expect(result.className).toContain("opacity-50"); - expect(result.className).toContain("backdrop-blur"); - - expect(result.style).toEqual({ width: "200px" }); - - expect(result.className).toContain( - "group-data-[windctrl-scope=header]/windctrl-scope:text-sm", - ); - }); - - it("should merge conflicting Tailwind classes (last one wins)", () => { - const button = windctrl({ - base: "text-red-500", - variants: { - intent: { - primary: "text-blue-500", - }, - }, - }); - - const result = button({ intent: "primary" }); - expect(result.className).toContain("text-blue-500"); - expect(result.className).not.toContain("text-red-500"); - }); - - it("should let Dynamic override Traits when Tailwind classes conflict (via twMerge)", () => { - const button = windctrl({ - traits: { - padded: "p-2", - }, - dynamic: { - p: (val) => (typeof val === "number" ? `p-${val}` : val), - }, - }); - - const result = button({ traits: ["padded"], p: 4 }); - - expect(result.className).toContain("p-4"); - expect(result.className).not.toContain("p-2"); - }); - - it("should let Traits override Base when Tailwind classes conflict (via twMerge)", () => { - const button = windctrl({ - base: "p-1", - traits: { padded: "p-3" }, - }); - - const result = button({ traits: ["padded"] }); - - expect(result.className).toContain("p-3"); - expect(result.className).not.toContain("p-1"); - }); - }); - - describe("Edge cases", () => { - it("should handle empty configuration", () => { - const button = windctrl({}); - const result = button({}); - expect(result.className).toBe(""); - expect(result.style).toEqual(undefined); - }); - - it("should handle undefined props gracefully", () => { - const button = windctrl({ - variants: { - intent: { - primary: "bg-blue-500", - }, - }, - }); - - const result = button({ intent: undefined as any }); - expect(result.className).not.toContain("bg-blue-500"); - }); - - it("should handle null props gracefully", () => { - const button = windctrl({ - variants: { - intent: { - primary: "bg-blue-500", - }, - }, - }); - - const result = button({ intent: null as any }); - expect(result.className).not.toContain("bg-blue-500"); - }); - - it("should handle traits with invalid keys gracefully", () => { - const button = windctrl({ - traits: { - loading: "opacity-50", - }, - }); - - const result = button({ traits: ["invalid-trait" as any] }); - expect(result.className).not.toContain("opacity-50"); - }); - }); - - describe("Type safety", () => { - it("should infer variant prop types correctly", () => { - const button = windctrl({ - variants: { - intent: { - primary: "bg-blue-500", - secondary: "bg-gray-500", - }, - }, - }); - - const result1 = button({ intent: "primary" }); - expect(result1.className).toContain("bg-blue-500"); - - const result2 = button({ intent: "secondary" }); - expect(result2.className).toContain("bg-gray-500"); - }); - - it("should infer trait keys correctly", () => { - const button = windctrl({ - traits: { - loading: "opacity-50", - glass: "backdrop-blur", - }, - }); - - const result1 = button({ traits: ["loading", "glass"] }); - expect(result1.className).toContain("opacity-50"); - expect(result1.className).toContain("backdrop-blur"); - - const result2 = button({ - traits: { loading: true, glass: false }, - }); - expect(result2.className).toContain("opacity-50"); - expect(result2.className).not.toContain("backdrop-blur"); - }); - - it("should infer dynamic prop types correctly", () => { - const button = windctrl({ - dynamic: { - w: (val) => - typeof val === "number" - ? { style: { width: `${val}px` } } - : `w-${val}`, - }, - }); - - const result1 = button({ w: "full" }); - expect(result1.className).toContain("w-full"); - - const result2 = button({ w: 100 }); - expect(result2.style).toEqual({ width: "100px" }); - }); - }); - - describe("Slots", () => { - it("should return slots as class strings and keep root as className/style", () => { - const button = windctrl({ - base: { - root: "rounded", - slots: { - icon: "shrink-0", - label: "truncate", - }, - }, - }); - - const result = button(); - - // root stays on className/style - expect(result.className).toContain("rounded"); - expect(result.style).toEqual(undefined); - - // slots exist as strings - expect(result.slots?.icon).toContain("shrink-0"); - expect(result.slots?.label).toContain("truncate"); - }); - - it("should apply variant slot classes based on prop value (and keep root variants working)", () => { - const button = windctrl({ - base: { - root: "inline-flex", - slots: { icon: "shrink-0" }, - }, - variants: { - size: { - sm: { - root: "h-8", - slots: { icon: "h-3 w-3" }, - }, - md: { - root: "h-10", - slots: { icon: "h-4 w-4" }, - }, - }, - }, - defaultVariants: { size: "md" }, - }); - - const sm = button({ size: "sm" }); - expect(sm.className).toContain("inline-flex"); - expect(sm.className).toContain("h-8"); - expect(sm.slots?.icon).toContain("h-3"); - expect(sm.slots?.icon).toContain("w-3"); - - const fallback = button({}); - expect(fallback.className).toContain("h-10"); - expect(fallback.slots?.icon).toContain("h-4"); - expect(fallback.slots?.icon).toContain("w-4"); - }); - - it("should apply trait slot classes (array form) and merge with base/variants", () => { - const button = windctrl({ - base: { - root: "inline-flex", - slots: { icon: "shrink-0" }, - }, - variants: { - size: { - sm: { slots: { icon: "h-3 w-3" } }, - }, - }, - traits: { - loading: { - root: "opacity-70", - slots: { icon: "animate-spin" }, - }, - }, - }); - - const result = button({ size: "sm", traits: ["loading"] }); - - // root gets trait too - expect(result.className).toContain("opacity-70"); - - // icon gets base + variant + trait - expect(result.slots?.icon).toContain("shrink-0"); - expect(result.slots?.icon).toContain("h-3"); - expect(result.slots?.icon).toContain("w-3"); - expect(result.slots?.icon).toContain("animate-spin"); - }); - - it("should let Traits override Variants on slots when Tailwind classes conflict (via twMerge)", () => { - const button = windctrl({ - variants: { - intent: { - primary: { slots: { icon: "text-blue-500" } }, - }, - }, - traits: { - dangerIcon: { slots: { icon: "text-red-500" } }, - }, - }); - - const result = button({ intent: "primary", traits: ["dangerIcon"] }); - - // last one wins: Traits > Variants - expect(result.slots?.icon).toContain("text-red-500"); - expect(result.slots?.icon).not.toContain("text-blue-500"); - }); - - it("should ignore invalid trait keys for slots gracefully (same behavior as root traits)", () => { - const button = windctrl({ - traits: { - loading: { slots: { icon: "animate-spin" } }, - }, - }); - - const result = button({ traits: ["invalid-trait" as any] }); - - expect(result.slots?.icon).toBe(undefined); - }); - - it("should not include slots when slots are not configured", () => { - const button = windctrl({ - base: "rounded", - variants: { - size: { - sm: "text-sm", - }, - }, - traits: { - loading: "opacity-50", - }, - }); - - const result = button({ size: "sm", traits: ["loading"] }); - - expect(result.className).toContain("rounded"); - expect(result.className).toContain("text-sm"); - expect(result.className).toContain("opacity-50"); - expect((result as any).slots).toBe(undefined); - }); - }); -}); diff --git a/src/index.ts b/src/index.ts index 11b5d79..b4eef36 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,78 +1,8 @@ import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; +import type { CSSProperties, DynamicResolver } from "./dynamic"; -type CSSProperties = Record; - -type DynamicResolverResult = - | string - | { className?: string; style?: CSSProperties }; - -type DynamicResolver = (value: T) => DynamicResolverResult; - -type PxProp = - | "width" - | "height" - | "minWidth" - | "maxWidth" - | "minHeight" - | "maxHeight" - | "top" - | "right" - | "bottom" - | "left"; - -type NumProp = "zIndex" | "flexGrow" | "flexShrink" | "order"; - -type VarUnit = "px" | "%" | "deg" | "ms"; - -function px(prop: PxProp): DynamicResolver { - return (value: number | string): DynamicResolverResult => { - if (typeof value === "number") { - return { style: { [prop]: `${value}px` } }; - } - return value; - }; -} - -function num(prop: NumProp): DynamicResolver { - return (value: number | string): DynamicResolverResult => { - if (typeof value === "number") { - return { style: { [prop]: value } }; - } - return value; - }; -} - -function opacity(): DynamicResolver { - return (value: number | string): DynamicResolverResult => { - if (typeof value === "number") { - return { style: { opacity: value } }; - } - return value; - }; -} - -function cssVar( - name: `--${string}`, - options?: { unit?: VarUnit }, -): DynamicResolver { - return (value: number | string): DynamicResolverResult => { - if (typeof value === "number") { - if (options?.unit) { - return { style: { [name]: `${value}${options.unit}` } }; - } - return { style: { [name]: String(value) } }; - } - return { style: { [name]: value } }; - }; -} - -export const dynamic = { - px, - num, - opacity, - var: cssVar, -}; +export { dynamic, type CSSProperties, type DynamicResolver } from "./dynamic"; type SlotAwareObject = { root?: ClassValue;