Skip to content

Commit 469d698

Browse files
committed
Feat: cx.multiple() for declare multiple classes
1 parent e9ebbb2 commit 469d698

File tree

2 files changed

+80
-1
lines changed

2 files changed

+80
-1
lines changed

packages/css/src/classname/cx.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { clsx } from "clsx";
2+
import type { ClassMultipleInput, ClassMultipleResult } from "./types.js";
23

34
/**
45
* Conditionally join class names into a single string
@@ -26,7 +27,23 @@ import { clsx } from "clsx";
2627
* cx('foo', [1 && 'bar', { baz: false }], ['hello', ['world']], 'cya');
2728
* // => 'foo bar hello world cya'
2829
*/
29-
export const cx = clsx;
30+
export const cx = Object.assign(clsx, {
31+
multiple: cxMultiple
32+
});
33+
34+
function cxMultiple<T extends ClassMultipleInput>(
35+
map: T
36+
): ClassMultipleResult<T> {
37+
const result = {} as ClassMultipleResult<T>;
38+
39+
for (const key in map) {
40+
if (Object.prototype.hasOwnProperty.call(map, key)) {
41+
result[key] = clsx(map[key]);
42+
}
43+
}
44+
45+
return result;
46+
}
3047

3148
// == Tests ====================================================================
3249
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -98,6 +115,11 @@ if (import.meta.vitest) {
98115
expect(cx("foo", 42, "bar")).toBe("foo 42 bar");
99116
});
100117

118+
it.skip("handles bigint inputs", () => {
119+
expect(cx(BigInt(123))).toBe("123");
120+
expect(cx("foo", BigInt(42), "bar")).toBe("foo 42 bar");
121+
});
122+
101123
it("filters falsy values correctly", () => {
102124
expect(cx(null)).toBe("");
103125
expect(cx(undefined)).toBe("");
@@ -127,4 +149,55 @@ if (import.meta.vitest) {
127149
assertType<string>(cx(123));
128150
});
129151
});
152+
153+
describe.concurrent("cx.multiple()", () => {
154+
it("processes a map of class values", () => {
155+
const result = cx.multiple({
156+
primary: ["bg-blue-500", "text-white"],
157+
secondary: ["bg-gray-500", "text-black"]
158+
});
159+
160+
expect(result.primary).toBe("bg-blue-500 text-white");
161+
expect(result.secondary).toBe("bg-gray-500 text-black");
162+
});
163+
164+
it("handles mixed input types in map", () => {
165+
const isHidden = false;
166+
const result = cx.multiple({
167+
strings: "foo bar",
168+
array: ["baz", "qux"],
169+
object: { enabled: true, disabled: false },
170+
conditional: ["base", isHidden && "hidden"]
171+
});
172+
173+
expect(result.strings).toBe("foo bar");
174+
expect(result.array).toBe("baz qux");
175+
expect(result.object).toBe("enabled");
176+
expect(result.conditional).toBe("base");
177+
});
178+
179+
it("handles empty map", () => {
180+
const result = cx.multiple({});
181+
expect(result).toEqual({});
182+
});
183+
184+
it("preserves keys with empty values", () => {
185+
const result = cx.multiple({
186+
empty: [],
187+
falsy: null
188+
});
189+
190+
expect(result.empty).toBe("");
191+
expect(result.falsy).toBe("");
192+
});
193+
194+
it("cx.multiple returns correct type", () => {
195+
const result = cx.multiple({
196+
a: "foo",
197+
b: ["bar"]
198+
});
199+
200+
assertType<{ a: string; b: string }>(result);
201+
});
202+
});
130203
}

packages/css/src/classname/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,9 @@ export type ClassDictionary = Record<string, unknown>;
2424
* Array of class values (supports nesting)
2525
*/
2626
export type ClassArray = ClassValue[];
27+
28+
export type ClassMultipleInput = Record<string, ClassValue>;
29+
30+
export type ClassMultipleResult<T extends ClassMultipleInput> = {
31+
[K in keyof T]: string;
32+
};

0 commit comments

Comments
 (0)