Skip to content

Commit d7cf9b5

Browse files
committed
Feat: cx function from clsx #285
1 parent 236463b commit d7cf9b5

File tree

6 files changed

+175
-1
lines changed

6 files changed

+175
-1
lines changed

packages/css/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@
8484
"prettier": "prettier-config-custom",
8585
"dependencies": {
8686
"@fastify/deepmerge": "^3.1.0",
87-
"@mincho-js/transform-to-vanilla": "workspace:^"
87+
"@mincho-js/transform-to-vanilla": "workspace:^",
88+
"clsx": "^2.1.1"
8889
},
8990
"devDependencies": {
9091
"@vanilla-extract/css": "^1.17.4",

packages/css/src/classname/cx.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { clsx } from "clsx";
2+
3+
/**
4+
* Conditionally join class names into a single string
5+
*
6+
* @param inputs - Class values to merge (strings, objects, arrays, or falsy values)
7+
* @returns Merged class name string with duplicates preserved
8+
*
9+
* @example
10+
* // Strings (variadic)
11+
* cx('foo', true && 'bar', 'baz');
12+
* // => 'foo bar baz'
13+
*
14+
* @example
15+
* // Objects
16+
* cx({ foo: true, bar: false, baz: isTrue() });
17+
* // => 'foo baz'
18+
*
19+
* @example
20+
* // Arrays (with nesting)
21+
* cx(['foo', 0, false, 'bar']);
22+
* // => 'foo bar'
23+
*
24+
* @example
25+
* // Kitchen sink
26+
* cx('foo', [1 && 'bar', { baz: false }], ['hello', ['world']], 'cya');
27+
* // => 'foo bar hello world cya'
28+
*/
29+
export const cx = clsx;
30+
31+
// == Tests ====================================================================
32+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
33+
// @ts-ignore error TS1343
34+
if (import.meta.vitest) {
35+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
36+
// @ts-ignore error TS1343
37+
const { describe, it, expect, assertType } = import.meta.vitest;
38+
39+
describe.concurrent("cx()", () => {
40+
it("handles string inputs (variadic)", () => {
41+
expect(cx("foo", "bar", "baz")).toBe("foo bar baz");
42+
expect(cx("foo")).toBe("foo");
43+
expect(cx("")).toBe("");
44+
});
45+
46+
it("handles conditional string inputs", () => {
47+
const isActive = true;
48+
const isHidden = false;
49+
expect(cx("foo", isActive && "bar", "baz")).toBe("foo bar baz");
50+
expect(cx("foo", isHidden && "bar", "baz")).toBe("foo baz");
51+
expect(cx("foo", null, "baz")).toBe("foo baz");
52+
expect(cx("foo", undefined, "baz")).toBe("foo baz");
53+
});
54+
55+
it("handles object inputs", () => {
56+
expect(cx({ foo: true, bar: false, baz: true })).toBe("foo baz");
57+
expect(cx({ foo: true })).toBe("foo");
58+
expect(cx({ foo: false })).toBe("");
59+
expect(cx({ foo: 1, bar: 0, baz: "truthy" })).toBe("foo baz");
60+
});
61+
62+
it("handles object inputs (variadic)", () => {
63+
expect(cx({ foo: true }, { bar: false }, null, { baz: "hello" })).toBe(
64+
"foo baz"
65+
);
66+
});
67+
68+
it("handles array inputs", () => {
69+
expect(cx(["foo", 0, false, "bar"])).toBe("foo bar");
70+
expect(cx(["foo"])).toBe("foo");
71+
expect(cx([null, undefined, false])).toBe("");
72+
});
73+
74+
it("handles array inputs (variadic)", () => {
75+
expect(
76+
cx(["foo"], ["", 0, false, "bar"], [["baz", [["hello"], "there"]]])
77+
).toBe("foo bar baz hello there");
78+
});
79+
80+
it("handles nested arrays", () => {
81+
expect(cx([["foo", [["bar"]]]])).toBe("foo bar");
82+
expect(cx(["foo", ["bar", ["baz"]]])).toBe("foo bar baz");
83+
});
84+
85+
it("handles kitchen sink (mixed inputs with nesting)", () => {
86+
const isVisible = true;
87+
expect(
88+
cx(
89+
"foo",
90+
[isVisible && "bar", { baz: false, bat: null }, ["hello", ["world"]]],
91+
"cya"
92+
)
93+
).toBe("foo bar hello world cya");
94+
});
95+
96+
it("handles number inputs", () => {
97+
expect(cx(1, 2, 3)).toBe("1 2 3");
98+
expect(cx("foo", 42, "bar")).toBe("foo 42 bar");
99+
});
100+
101+
it("filters falsy values correctly", () => {
102+
expect(cx(null)).toBe("");
103+
expect(cx(undefined)).toBe("");
104+
expect(cx(false)).toBe("");
105+
expect(cx(0)).toBe("");
106+
expect(cx("")).toBe("");
107+
expect(cx(null, undefined, false, 0, "")).toBe("");
108+
});
109+
110+
it("preserves whitespace in class names", () => {
111+
expect(cx("foo bar")).toBe("foo bar");
112+
});
113+
114+
it("handles empty inputs", () => {
115+
expect(cx()).toBe("");
116+
expect(cx([])).toBe("");
117+
expect(cx({})).toBe("");
118+
});
119+
120+
it("accepts valid input types", () => {
121+
assertType<string>(cx("foo"));
122+
assertType<string>(cx("foo", "bar"));
123+
assertType<string>(cx({ foo: true }));
124+
assertType<string>(cx(["foo", "bar"]));
125+
assertType<string>(cx("foo", { bar: true }, ["baz"]));
126+
assertType<string>(cx(null, undefined, false));
127+
assertType<string>(cx(123));
128+
});
129+
});
130+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { cx } from "./cx.js";
2+
3+
export type { ClassValue, ClassArray, ClassDictionary } from "./types.js";
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// == cx() Types ===============================================================
2+
// https://github.com/lukeed/clsx/blob/master/clsx.d.mts
3+
/**
4+
* Valid class value types for cx()
5+
* Supports strings, numbers, objects, arrays, and falsy values
6+
*/
7+
export type ClassValue =
8+
| ClassArray
9+
| ClassDictionary
10+
| string
11+
| number
12+
| bigint
13+
| null
14+
| boolean
15+
| undefined;
16+
17+
/**
18+
* Object with class names as keys and boolean conditions as values
19+
* @example { 'bg-blue-500': true, 'text-white': isActive }
20+
*/
21+
export type ClassDictionary = Record<string, unknown>;
22+
23+
/**
24+
* Array of class values (supports nesting)
25+
*/
26+
export type ClassArray = ClassValue[];

packages/css/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,9 @@ export type {
5858
TokenNumberDefinition,
5959
ResolveTheme
6060
} from "./theme/types.js";
61+
export { cx } from "./classname/index.js";
62+
export type {
63+
ClassValue,
64+
ClassArray,
65+
ClassDictionary
66+
} from "./classname/index.js";

yarn.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1431,6 +1431,7 @@ __metadata:
14311431
"@fastify/deepmerge": "npm:^3.1.0"
14321432
"@mincho-js/transform-to-vanilla": "workspace:^"
14331433
"@vanilla-extract/css": "npm:^1.17.4"
1434+
clsx: "npm:^2.1.1"
14341435
eslint-config-custom: "workspace:^"
14351436
prettier-config-custom: "workspace:^"
14361437
tsconfig-custom: "workspace:^"
@@ -3086,6 +3087,13 @@ __metadata:
30863087
languageName: node
30873088
linkType: hard
30883089

3090+
"clsx@npm:^2.1.1":
3091+
version: 2.1.1
3092+
resolution: "clsx@npm:2.1.1"
3093+
checksum: 10/cdfb57fa6c7649bbff98d9028c2f0de2f91c86f551179541cf784b1cfdc1562dcb951955f46d54d930a3879931a980e32a46b598acaea274728dbe068deca919
3094+
languageName: node
3095+
linkType: hard
3096+
30893097
"color-convert@npm:^2.0.1":
30903098
version: 2.0.1
30913099
resolution: "color-convert@npm:2.0.1"

0 commit comments

Comments
 (0)