Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/css/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@
"prettier": "prettier-config-custom",
"dependencies": {
"@fastify/deepmerge": "^3.1.0",
"@mincho-js/transform-to-vanilla": "workspace:^"
"@mincho-js/transform-to-vanilla": "workspace:^",
"clsx": "^2.1.1"
},
"devDependencies": {
"@vanilla-extract/css": "^1.17.4",
Expand Down
130 changes: 130 additions & 0 deletions packages/css/src/classname/cx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { clsx } from "clsx";

/**
* Conditionally join class names into a single string
*
* @param inputs - Class values to merge (strings, objects, arrays, or falsy values)
* @returns Merged class name string with duplicates preserved
*
* @example
* // Strings (variadic)
* cx('foo', true && 'bar', 'baz');
* // => 'foo bar baz'
*
* @example
* // Objects
* cx({ foo: true, bar: false, baz: isTrue() });
* // => 'foo baz'
*
* @example
* // Arrays (with nesting)
* cx(['foo', 0, false, 'bar']);
* // => 'foo bar'
*
* @example
* // Kitchen sink
* cx('foo', [1 && 'bar', { baz: false }], ['hello', ['world']], 'cya');
* // => 'foo bar hello world cya'
*/
export const cx = clsx;

// == Tests ====================================================================
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore error TS1343
if (import.meta.vitest) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore error TS1343
const { describe, it, expect, assertType } = import.meta.vitest;

describe.concurrent("cx()", () => {
it("handles string inputs (variadic)", () => {
expect(cx("foo", "bar", "baz")).toBe("foo bar baz");
expect(cx("foo")).toBe("foo");
expect(cx("")).toBe("");
});

it("handles conditional string inputs", () => {
const isActive = true;
const isHidden = false;
expect(cx("foo", isActive && "bar", "baz")).toBe("foo bar baz");
expect(cx("foo", isHidden && "bar", "baz")).toBe("foo baz");
expect(cx("foo", null, "baz")).toBe("foo baz");
expect(cx("foo", undefined, "baz")).toBe("foo baz");
});

it("handles object inputs", () => {
expect(cx({ foo: true, bar: false, baz: true })).toBe("foo baz");
expect(cx({ foo: true })).toBe("foo");
expect(cx({ foo: false })).toBe("");
expect(cx({ foo: 1, bar: 0, baz: "truthy" })).toBe("foo baz");
});

it("handles object inputs (variadic)", () => {
expect(cx({ foo: true }, { bar: false }, null, { baz: "hello" })).toBe(
"foo baz"
);
});

it("handles array inputs", () => {
expect(cx(["foo", 0, false, "bar"])).toBe("foo bar");
expect(cx(["foo"])).toBe("foo");
expect(cx([null, undefined, false])).toBe("");
});

it("handles array inputs (variadic)", () => {
expect(
cx(["foo"], ["", 0, false, "bar"], [["baz", [["hello"], "there"]]])
).toBe("foo bar baz hello there");
});

it("handles nested arrays", () => {
expect(cx([["foo", [["bar"]]]])).toBe("foo bar");
expect(cx(["foo", ["bar", ["baz"]]])).toBe("foo bar baz");
});

it("handles kitchen sink (mixed inputs with nesting)", () => {
const isVisible = true;
expect(
cx(
"foo",
[isVisible && "bar", { baz: false, bat: null }, ["hello", ["world"]]],
"cya"
)
).toBe("foo bar hello world cya");
});

it("handles number inputs", () => {
expect(cx(1, 2, 3)).toBe("1 2 3");
expect(cx("foo", 42, "bar")).toBe("foo 42 bar");
});

it("filters falsy values correctly", () => {
expect(cx(null)).toBe("");
expect(cx(undefined)).toBe("");
expect(cx(false)).toBe("");
expect(cx(0)).toBe("");
expect(cx("")).toBe("");
expect(cx(null, undefined, false, 0, "")).toBe("");
});

it("preserves whitespace in class names", () => {
expect(cx("foo bar")).toBe("foo bar");
});

it("handles empty inputs", () => {
expect(cx()).toBe("");
expect(cx([])).toBe("");
expect(cx({})).toBe("");
});

it("accepts valid input types", () => {
assertType<string>(cx("foo"));
assertType<string>(cx("foo", "bar"));
assertType<string>(cx({ foo: true }));
assertType<string>(cx(["foo", "bar"]));
assertType<string>(cx("foo", { bar: true }, ["baz"]));
assertType<string>(cx(null, undefined, false));
assertType<string>(cx(123));
});
});
}
3 changes: 3 additions & 0 deletions packages/css/src/classname/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { cx } from "./cx.js";

export type { ClassValue, ClassArray, ClassDictionary } from "./types.js";
26 changes: 26 additions & 0 deletions packages/css/src/classname/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// == cx() Types ===============================================================
// https://github.com/lukeed/clsx/blob/master/clsx.d.mts
/**
* Valid class value types for cx()
* Supports strings, numbers, objects, arrays, and falsy values
*/
export type ClassValue =
| ClassArray
| ClassDictionary
| string
| number
| bigint
| null
| boolean
| undefined;

/**
* Object with class names as keys and boolean conditions as values
* @example { 'bg-blue-500': true, 'text-white': isActive }
*/
export type ClassDictionary = Record<string, unknown>;

Comment on lines +17 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Docs/type mismatch: ClassDictionary values aren’t necessarily boolean.
Either tighten the type to booleans (and maybe ClassValue) or adjust the doc to “truthy/falsey conditions”.

Proposed doc fix (minimal)
 /**
- * Object with class names as keys and boolean conditions as values
+ * Object with class names as keys and truthy/falsey conditions as values
  * @example { 'bg-blue-500': true, 'text-white': isActive }
  */
 export type ClassDictionary = Record<string, unknown>;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Object with class names as keys and boolean conditions as values
* @example { 'bg-blue-500': true, 'text-white': isActive }
*/
export type ClassDictionary = Record<string, unknown>;
/**
* Object with class names as keys and truthy/falsey conditions as values
* @example { 'bg-blue-500': true, 'text-white': isActive }
*/
export type ClassDictionary = Record<string, unknown>;
🤖 Prompt for AI Agents
In @packages/css/src/classname/types.ts around lines 17 - 22, The doc/type
mismatch: update the ClassDictionary type to enforce boolean values by replacing
export type ClassDictionary = Record<string, unknown>; with export type
ClassDictionary = Record<string, boolean>; and adjust the JSDoc/example to
reflect boolean conditions (e.g., { 'bg-blue-500': true, 'text-white': false })
so the type and comment are consistent; if you prefer to allow truthy/falsy
values instead, alternatively change the JSDoc wording to “truthy/falsey
conditions” rather than tightening the type.

/**
* Array of class values (supports nesting)
*/
export type ClassArray = ClassValue[];
6 changes: 6 additions & 0 deletions packages/css/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,9 @@ export type {
TokenNumberDefinition,
ResolveTheme
} from "./theme/types.js";
export { cx } from "./classname/index.js";
export type {
ClassValue,
ClassArray,
ClassDictionary
} from "./classname/index.js";
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1431,6 +1431,7 @@ __metadata:
"@fastify/deepmerge": "npm:^3.1.0"
"@mincho-js/transform-to-vanilla": "workspace:^"
"@vanilla-extract/css": "npm:^1.17.4"
clsx: "npm:^2.1.1"
eslint-config-custom: "workspace:^"
prettier-config-custom: "workspace:^"
tsconfig-custom: "workspace:^"
Expand Down Expand Up @@ -3086,6 +3087,13 @@ __metadata:
languageName: node
linkType: hard

"clsx@npm:^2.1.1":
version: 2.1.1
resolution: "clsx@npm:2.1.1"
checksum: 10/cdfb57fa6c7649bbff98d9028c2f0de2f91c86f551179541cf784b1cfdc1562dcb951955f46d54d930a3879931a980e32a46b598acaea274728dbe068deca919
languageName: node
linkType: hard

"color-convert@npm:^2.0.1":
version: 2.0.1
resolution: "color-convert@npm:2.0.1"
Expand Down
Loading