diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..664d562 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,55 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": [ + "plugin:prettier/recommended", + "plugin:@typescript-eslint/recommended" + ], + "plugins": [ + "prettier" + ], + "parserOptions": { + "ecmaVersion": 12, + "sourceType": "module" + }, + "env": { + "es2021": true, + "node": true + }, + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/ban-ts-comment": "off", + "no-console": "warn", + "no-throw-literal": "error", + "complexity": [ + "warn", + 20 + ], + "eqeqeq": [ + "error", + "always" + ], + "no-unused-vars": [ + "off" + ], + "no-var": "error", + "prefer-const": "error", + "prefer-arrow-callback": "error", + "semi": "off", + "quotes": [ + "error", + "double" + ] + }, + "settings": { + "import/resolver": { + "node": { + "extensions": [ + ".js", + ".jsx", + ".ts", + ".tsx" + ] + } + } + } +} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..486961e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "singleQuote": false, + "tabWidth": 2, + "useTabs": false, + "semi": false, + "printWidth": 120 +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d5c5ded..99e075e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,10 +23,10 @@ Thank you for considering contributing to `@maphel/classnames`. We appreciate yo 1. **Fork the Repository** - Fork the `@maphel/classnames` repository on GitHub and clone your fork locally. + Fork the `@maphel/classes` repository on GitHub and clone your fork locally. ```sh - git clone https://github.com/[YOUR_USERNAME]/classnames.git + git clone https://github.com/maphel/classes.git ``` 2. **Install Dependencies** @@ -34,8 +34,8 @@ Thank you for considering contributing to `@maphel/classnames`. We appreciate yo Navigate to the repository folder and install all necessary packages: ```sh - cd classnames - npm install + cd classes + yarn install ``` 3. **Branching** @@ -54,8 +54,9 @@ Thank you for considering contributing to `@maphel/classnames`. We appreciate yo - Make sure your code lints and all tests pass. Run: ```sh - npm run lint - npm test + yarn lint + yarn test + yarn prettier ``` - Keep your code as clean and straightforward as possible. @@ -97,5 +98,5 @@ Thank you for considering contributing to `@maphel/classnames`. We appreciate yo ## License -By contributing to `@maphel/classnames`, you agree that your contributions will be licensed under its MIT License. +By contributing to `@maphel/classes`, you agree that your contributions will be licensed under its MIT License. diff --git a/README.md b/README.md index 7e71cb8..487f55c 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -# `@maphel/classnames` +# `@maphel/classes` -![Classnames Package](https://img.shields.io/badge/@maphel-classnames-8A2BE2) -![NPM version](https://img.shields.io/npm/v/@maphel/classnames.svg) -![Build Status](https://img.shields.io/github/actions/workflow/status/maphel/classnames/build.yml) -![Coverage Status](https://img.shields.io/coverallsCoverage/github/maphel/classnames) -![License](https://img.shields.io/github/license/maphel/classnames) +![Classnames Package](https://img.shields.io/badge/@maphel-classes-8A2BE2) +![NPM version](https://img.shields.io/npm/v/@maphel/classes.svg) +![Build Status](https://img.shields.io/github/actions/workflow/status/maphel/classes/build.yml) +![Coverage Status](https://img.shields.io/coverallsCoverage/github/maphel/classes) +![License](https://img.shields.io/github/license/maphel/classes) -> The `@maphel/classnames` package is a TypeScript utility for easily managing CSS class names in your JavaScript or TypeScript application. Ideal for frontend frameworks like React, Angular, and Vue, it lets you dynamically generate className strings using conditions. Merge multiple class names, use conditionals, and manage UI state effortlessly. +> The `@maphel/classes` package is a TypeScript utility for easily managing CSS class names in your JavaScript or TypeScript application. Ideal for frontend frameworks like React, Angular, and Vue, it lets you dynamically generate className strings using conditions. Merge multiple class names, use conditionals, and manage UI state effortlessly. --- @@ -28,27 +28,34 @@ Type Safety: Being written in TypeScript, it offers type safety while handling c Conditional Classes: Easily apply class names based on conditions. Array and Object Support: Accepts an array or object of class names, giving you flexibility in how you manage your classes. -## Why @maphel/classnames? -Working with dynamic class names often leads to cumbersome ternary or logical operations that can make the code less readable. The @maphel/classnames utility helps you create class name strings in a more expressive, readable, and error-free manner. +## Why @maphel/classes? +Working with dynamic class names often leads to cumbersome ternary or logical operations that can make the code less readable. The @maphel/classes utility helps you create class name strings in a more expressive, readable, and error-free manner. --- ## Installation ```bash -npm install @maphel/classnames +npm install @maphel/classes + +yarn add @maphel/classes ``` ## Usage -Import `classNames` into your TypeScript or JavaScript file. +Import `@maphel/classes` into your TypeScript or JavaScript file. ```typescript -import { cn } from '@maphel/classnames'; -import { classNames } from '@maphel/classnames'; +import { c } from '@maphel/classes'; +import classes from '@maphel/classes'; + +var c = require('@maphel/classes').c; +var classes = require('@maphel/classes').default; ``` ## API ```typescript -cn(...args: ClassValue[]): string; -classNames(...args: ClassValue[]): string; +type ClassProps = string | boolean | { [key: string]: string | boolean } | ClassProps[]; + +c(...args: ClassProps[]): string; +classes(...args: ClassProps[]): string; ``` ### Parameters @@ -62,22 +69,22 @@ Returns a concatenated string of class names based on `args`. ## Examples ```typescript -import { cn } from '@maphel/classnames'; +import { c } from '@maphel/classes'; // Basic Usage -const basicResult = cn('class1', 'class2'); +const basicResult = c('class1', 'class2'); console.log(basicResult); // Output: "class1 class2" // Conditional Usage -const conditionalResult = cn('base', { 'active': true, 'disabled': false }); +const conditionalResult = c('base', { 'active': true, 'disabled': false }); console.log(conditionalResult); // Output: "base active" // Array Usage -const arrayResult = cn(['array-class1', 'array-class2']); +const arrayResult = c(['array-class1', 'array-class2']); console.log(arrayResult); // Output: "array-class1 array-class2" // Exessive Usage -const exessiveResult = cn('a', ['b', 'c'], {d: true}, ['e', {f: true}, ' g', 'h '], [' i ', [{' j': true}]], ' k', 'l ', ' m '); +const exessiveResult = c('a', ['b', 'c'], {d: true}, ['e', {f: true}, ' g', 'h '], [' i ', [{' j': true}]], ' k', 'l ', ' m '); console.log(exessiveResult); // Output: "a b c d e f g h i j k l m" ``` --- diff --git a/package.json b/package.json index 8d4e6c0..41c5795 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@maphel/classnames", + "name": "@maphel/classes", "version": "0.0.1", "description": "A utility for generating class names", "main": "./dist/index.js", @@ -8,23 +8,32 @@ "scripts": { "test": "jest --coverage", "build": "tsc", - "prepublish": "yarn build" + "prepublish": "yarn build", + "lint": "eslint src/**/*.{js,jsx,ts,tsx} --fix", + "prettier": "prettier --write \"src/**/*.ts\"" }, "devDependencies": { "@types/jest": "^29.5.4", + "@typescript-eslint/eslint-plugin": "^6.5.0", + "@typescript-eslint/parser": "^6.5.0", "coveralls": "^3.1.1", + "eslint": "8.22.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", "jest": "^29.6.4", + "prettier": "^3.0.3", "ts-jest": "^29.1.1", "typescript": "^5.2.2" }, "repository": { "type": "git", - "url": "https://github.com/maphel/classnames.git" + "url": "https://github.com/maphel/classes.git" }, "author": "", "license": "MIT", "bugs": { - "url": "https://github.com/maphel/classnames/issues" + "url": "https://github.com/maphel/classes/issues" }, - "homepage": "https://github.com/maphel/classnames#readme" + "homepage": "https://github.com/maphel/classes#readme", + "packageManager": "yarn@3.6.3" } diff --git a/src/classes/index.test.ts b/src/classes/index.test.ts new file mode 100644 index 0000000..73cb4a7 --- /dev/null +++ b/src/classes/index.test.ts @@ -0,0 +1,136 @@ +import { c } from "./index" + +describe("c", () => { + it("should return an empty string if no arguments are provided", () => { + expect(c()).toBe("") + }) + + it("should ignore undefined, null, and boolean arguments", () => { + expect(c(undefined, null, false, true)).toBe("") + }) + + it("should include string arguments", () => { + expect(c("class1", "class2")).toBe("class1 class2") + }) + + it("should filter out false keys in ClassMap and include true keys", () => { + expect(c({ class1: true, class2: false })).toBe("class1") + }) + + it("should filter out false keys in ClassMap and include true keys and add class3", () => { + expect(c({ class1: true, class2: false }, "class3")).toBe("class1 class3") + }) + + it("should filter out false keys in ClassMap and include true keys and add class3", () => { + expect(c({ class1: true, class2: false }, undefined, "class3")).toBe("class1 class3") + }) + + it("should handle a combination of all types of arguments", () => { + expect( + c( + "class1", + undefined, + null, + { + class2: true, + class3: false, + }, + "class4", + ), + ).toBe("class1 class2 class4") + }) + + it('should ignore object arguments with non-boolean values except strings like "true" or "false" or "1" or "2"', () => { + expect(c({ class1: "true", class2: 123, class3: "1" } as any)).toBe("class1 class3") + }) + + it("should only include keys with boolean true values in mixed objects", () => { + expect(c({ class1: true, class2: "false", class3: 0 } as any)).toBe("class1") + }) + + it("should ignore empty strings", () => { + expect(c("class1", "", "class2")).toBe("class1 class2") + }) + + it("should include classes names with whitespace", () => { + expect(c("class1", "classes two")).toBe("class1 classes two") + }) + + it("should ignore number classes names", () => { + expect(c("class1", 123 as any, "class2")).toBe("class1 class2") + }) + + it("should ignore array inputs", () => { + expect(c("class1", ["class2", "class3"] as any)).toBe("class1 class2 class3") + }) + + it("should ignore nested objects", () => { + expect(c({ class1: true, nested: { class2: true } } as any)).toBe("class1") + }) + + it("should include classes names with leading or trailing whitespaces as-is", () => { + expect(c(" class1 ", "class2 ")).toBe("class1 class2") + }) + + it("should include classes names with array", () => { + expect(c(["class1", "class2"])).toBe("class1 class2") + }) + + it("should include classes names with array and string as 2nd parameter", () => { + expect(c(["class1", "class2"], "class3")).toBe("class1 class2 class3") + }) + + it("should include classes names with array with object and string additional parameter", () => { + expect(c(["class1", "class2"], "class3", { class4: true })).toBe("class1 class2 class3 class4") + }) + + it("should filter out false keys in ClassMap and include true keys", () => { + expect(c({ class1: true, class2: false })).toBe("class1") + }) + + it("evaluate all features", () => { + expect( + c("a", ["b", "c"], { d: true }, ["e", { f: true }, " g", "h "], [" i ", [{ " j": true }]], " k", "l ", " m "), + ).toBe("a b c d e f g h i j k l m") + }) + + it("should return empty string for no arguments", () => { + expect(c()).toBe("") + }) + + it("should concatenate classes names", () => { + expect(c("a", "b")).toBe("a b") + }) + + it("should omit falsy values", () => { + expect(c("a", null, false, "b")).toBe("a b") + }) + + it("should include truthy values from an object", () => { + expect(c("a", { b: true, c: false }, "d")).toBe("a b d") + }) + + it("should work with arrays", () => { + expect(c(["a", "b"])).toBe("a b") + }) + + it("should work with nested arrays", () => { + expect(c(["a", ["b", "c"]])).toBe("a b c") + }) + + it("should work with arrays containing objects", () => { + expect(c(["a", { b: true }])).toBe("a b") + }) + + it("should work with deeply nested structures", () => { + expect(c(["a", ["b", { c: true }], [["d", { e: true }]]])).toBe("a b c d e") + }) + + it("should work with only one object", () => { + expect(c({ e: true })).toBe("e") + }) + + it("should work with only one string", () => { + expect(c("e")).toBe("e") + }) +}) diff --git a/src/classes/index.ts b/src/classes/index.ts new file mode 100644 index 0000000..b1d60c6 --- /dev/null +++ b/src/classes/index.ts @@ -0,0 +1,60 @@ +export type ClassProps = string | boolean | { [key: string]: string | boolean } | ClassProps[] + +export function c(...args: ClassProps[]): string { + const classes: string[] = [] + + for (let i = 0; i < args.length; i++) { + processArg(args[i], classes) + } + + let finalClasses = "" + let isFirstClass = true + + for (let i = 0; i < classes.length; i++) { + if (classes[i] === "true") { + continue + } + + const trimmedClass = classes[i].trim() + + if (trimmedClass) { + if (!isFirstClass) { + finalClasses += " " + } else { + isFirstClass = false + } + finalClasses += trimmedClass + } + } + + return finalClasses +} + +function processArg(arg: ClassProps, classes: string[]): void { + if (arg === undefined || arg === null || arg === false) { + return + } + + if (Array.isArray(arg)) { + for (let i = 0; i < arg.length; i++) { + processArg(arg[i], classes) + } + return + } + + if (typeof arg === "object") { + for (const key in arg) { + if (Object.prototype.hasOwnProperty.call(arg, key)) { + const value = arg[key] + if (value === true || value === "true" || value === "1") { + classes.push(key) + } + } + } + return + } + + if (typeof arg === "string" || typeof arg === "boolean") { + classes.push(arg.toString()) + } +} diff --git a/src/classnames/index.test.ts b/src/classnames/index.test.ts deleted file mode 100644 index eb3ff6f..0000000 --- a/src/classnames/index.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -import {cn} from "./index"; - -describe("cn", () => { - it('should return an empty string if no arguments are provided', () => { - expect(cn()).toBe(''); - }); - - it('should ignore undefined, null, and boolean arguments', () => { - expect(cn(undefined, null, false, true)).toBe(''); - }); - - it('should include string arguments', () => { - expect(cn('class1', 'class2')).toBe('class1 class2'); - }); - - it('should filter out false keys in ClassMap and include true keys', () => { - expect(cn({class1: true, class2: false})).toBe('class1'); - }); - - it('should filter out false keys in ClassMap and include true keys and add class3', () => { - expect(cn({class1: true, class2: false}, "class3")).toBe('class1 class3'); - }); - - it('should filter out false keys in ClassMap and include true keys and add class3', () => { - expect(cn({class1: true, class2: false}, undefined, "class3")).toBe('class1 class3'); - }); - - it('should handle a combination of all types of arguments', () => { - expect(cn('class1', undefined, null, { - class2: true, - class3: false - }, 'class4')).toBe('class1 class2 class4'); - }); - - it('should ignore object arguments with non-boolean values except strings like "true" or "false" or "1" or "2"', () => { - expect(cn({class1: "true", class2: 123, class3: "1"} as any)).toBe('class1 class3'); - }); - - it('should only include keys with boolean true values in mixed objects', () => { - expect(cn({class1: true, class2: "false", class3: 0} as any)).toBe('class1'); - }); - - it('should ignore empty strings', () => { - expect(cn('class1', '', 'class2')).toBe('class1 class2'); - }); - - it('should include class names with whitespace', () => { - expect(cn('class1', 'class two')).toBe('class1 class two'); - }); - - it('should ignore number class names', () => { - expect(cn('class1', 123 as any, 'class2')).toBe('class1 class2'); - }); - - it('should ignore array inputs', () => { - expect(cn('class1', ['class2', 'class3'] as any)).toBe('class1 class2 class3'); - }); - - it('should ignore nested objects', () => { - expect(cn({class1: true, nested: {class2: true}} as any)).toBe('class1'); - }); - - it('should include class names with leading or trailing whitespaces as-is', () => { - expect(cn(' class1 ', 'class2 ')).toBe('class1 class2'); - }); - - it('should include class names with array', () => { - expect(cn(['class1', 'class2'])).toBe('class1 class2'); - }); - - it('should include class names with array and string as 2nd parameter', () => { - expect(cn(['class1', 'class2'], "class3")).toBe('class1 class2 class3'); - }); - - it('should include class names with array with object and string additional parameter', () => { - expect(cn(['class1', 'class2'], "class3", {class4: true})).toBe('class1 class2 class3 class4'); - }); - - it('should filter out false keys in ClassMap and include true keys', () => { - expect(cn({class1: true, class2: false})).toBe('class1'); - }); - - it('evaluate all features', () => { - expect(cn('a', ['b', 'c'], {d: true}, ['e', {f: true}, ' g', 'h '], [' i ', [{' j': true}]], ' k', 'l ', ' m ')).toBe('a b c d e f g h i j k l m'); - }); - - it('should return empty string for no arguments', () => { - expect(cn()).toBe(''); - }); - - it('should concatenate class names', () => { - expect(cn('a', 'b')).toBe('a b'); - }); - - it('should omit falsy values', () => { - expect(cn('a', null, false, 'b')).toBe('a b'); - }); - - it('should include truthy values from an object', () => { - expect(cn('a', {'b': true, 'c': false}, 'd')).toBe('a b d'); - }); - - it('should work with arrays', () => { - expect(cn(['a', 'b'])).toBe('a b'); - }); - - it('should work with nested arrays', () => { - expect(cn(['a', ['b', 'c']])).toBe('a b c'); - }); - - it('should work with arrays containing objects', () => { - expect(cn(['a', {'b': true}])).toBe('a b'); - }); - - it('should work with deeply nested structures', () => { - expect(cn(['a', ['b', {'c': true}], [['d', {'e': true}]]])).toBe('a b c d e'); - }); - - it('should work with only one object', () => { - expect(cn({'e': true})).toBe('e'); - }); - - it('should work with only one string', () => { - expect(cn("e")).toBe('e'); - }); -}); diff --git a/src/classnames/index.ts b/src/classnames/index.ts deleted file mode 100644 index 0b6db4c..0000000 --- a/src/classnames/index.ts +++ /dev/null @@ -1,66 +0,0 @@ -export type ClassValue = string | boolean | ClassValueArray | ClassMap; -export type ClassValueArray = ClassValue[]; -export type ClassMap = { [key: string]: string | boolean }; - -export const classNames = cn; - -export function cn(...args: ClassValue[]): string { - const classes: string[] = []; - - for (let i = 0; i < args.length; i++) { - processArg(args[i], classes); - } - - let finalClasses = ''; - let isFirstClass = true; - - for (let i = 0; i < classes.length; i++) { - // Ignore boolean 'true' - if (classes[i] === 'true') { - continue; - } - - // Trim each class - const trimmedClass = classes[i].trim(); - - if (trimmedClass) { - if (!isFirstClass) { - finalClasses += ' '; - } else { - isFirstClass = false; - } - finalClasses += trimmedClass; - } - } - - return finalClasses; -} - -function processArg(arg: ClassValue, classes: string[]): void { - if (arg === undefined || arg === null || arg === false) { - return; - } - - if (Array.isArray(arg)) { - for (let i = 0; i < arg.length; i++) { - processArg(arg[i], classes); - } - return; - } - - if (typeof arg === 'object') { - for (const key in arg) { - if (Object.prototype.hasOwnProperty.call(arg, key)) { - const value = arg[key]; - if (value === true || value === 'true' || value === '1') { - classes.push(key); - } - } - } - return; - } - - if (typeof arg === 'string' || typeof arg === 'boolean') { - classes.push(arg.toString()); - } -} diff --git a/src/index.ts b/src/index.ts index ece1003..e1d50b0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,4 @@ -export {cn, classNames, ClassMap, ClassValueArray} from "./classnames" +import { c } from "./classes/index" + +export default c +export { c, ClassProps } from "./classes"