clases 1.0.2 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -1
- package/dist/index.cjs +9 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -17
- package/dist/index.d.ts +5 -17
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +48 -38
- package/src/test/cn.test.ts +99 -34
package/README.md
CHANGED
|
@@ -63,8 +63,71 @@ cl({
|
|
|
63
63
|
});
|
|
64
64
|
// Result: "md:hover:scale-105 md:hover:after:content-['*']"
|
|
65
65
|
```
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 🌈 Beautiful Syntax & Variants
|
|
69
|
+
|
|
70
|
+
The most powerful feature of this utility is **Transparent Logical Nesting**. It allows you to organize your design system using nested objects that represent your business logic (variants, states, or themes) without polluting the final CSS output.
|
|
71
|
+
|
|
72
|
+
#### How it Works
|
|
73
|
+
|
|
74
|
+
The engine distinguishes between **Registered Prefixes** (modifiers like `md`, `hover`, or `ui`) and **Logical Keys** (your own organizational names like `variants`, `primary`, or `[state]`):
|
|
75
|
+
|
|
76
|
+
* **Registered Keys**: Concatenate to form the final CSS prefix.
|
|
77
|
+
* **Unregistered Keys**: Act as transparent wrappers. They are ignored in the final string but pass the current prefix down to their children.
|
|
78
|
+
|
|
79
|
+
#### Component Variants Example
|
|
66
80
|
|
|
81
|
+
This structure allows you to colocate base styles, responsive modifiers, and interaction states within a single logical branch:
|
|
67
82
|
|
|
83
|
+
```typescript
|
|
84
|
+
const variant = 'primary';
|
|
85
|
+
const theme = 'dark';
|
|
86
|
+
|
|
87
|
+
const className = cl({
|
|
88
|
+
md: {
|
|
89
|
+
// 'variants' is NOT in the registry, so it is transparent
|
|
90
|
+
variants: {
|
|
91
|
+
// We select the active branch using standard JS
|
|
92
|
+
[variant]: {
|
|
93
|
+
base: 'rounded-lg px-4 py-2 transition',
|
|
94
|
+
// 'dark' is a registered prefix, so it will be mapped
|
|
95
|
+
dark: 'border-white text-white',
|
|
96
|
+
hover: 'opacity-80'
|
|
97
|
+
},
|
|
98
|
+
secondary: 'bg-gray-200 text-black'
|
|
99
|
+
}[variant]
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Output (for variant 'primary'):
|
|
105
|
+
* "md:rounded-lg md:px-4 md:py-2 md:transition md:dark:border-white md:dark:text-white md:hover:opacity-80"
|
|
106
|
+
*/
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### Why this is superior:
|
|
110
|
+
|
|
111
|
+
1. **Clean DOM**: You won't see "ghost" prefixes like `variants:primary:bg-blue-500` in your HTML.
|
|
112
|
+
2. **Zero Boilerplate**: You don't have to repeat `md:dark:...` for every single class; the engine handles the chain automatically.
|
|
113
|
+
3. **Type-Safe Organization**: Use your own naming conventions to group styles while keeping the output perfectly compatible with Tailwind CSS.
|
|
114
|
+
|
|
115
|
+
#### Best Practice: Selection Logic
|
|
116
|
+
|
|
117
|
+
To keep the output optimized and prevent class collisions, handle the selection at the logical level so the engine only processes the "winning" branch:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
cl({
|
|
121
|
+
ui: {
|
|
122
|
+
[status]: {
|
|
123
|
+
success: 'text-green-600',
|
|
124
|
+
error: 'text-red-600',
|
|
125
|
+
pending: 'text-yellow-600'
|
|
126
|
+
}[status]
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
---
|
|
68
131
|
|
|
69
132
|
### 🛠️ Custom Plugin Management
|
|
70
133
|
You can stack the Tailwind plugin with your own semantic aliases or project-specific configs.
|
|
@@ -100,7 +163,6 @@ cl({
|
|
|
100
163
|
lg: 'grid-cols-3 gap-8'
|
|
101
164
|
});
|
|
102
165
|
```
|
|
103
|
-
|
|
104
166
|
---
|
|
105
167
|
|
|
106
168
|
## ⌨️ Why Objects?
|
package/dist/index.cjs
CHANGED
|
@@ -17,13 +17,18 @@ function createCl(...plugins) {
|
|
|
17
17
|
}
|
|
18
18
|
if (typeof value === "object") {
|
|
19
19
|
return Object.entries(value).map(([nestedKey, nestedValue]) => {
|
|
20
|
-
const
|
|
21
|
-
|
|
20
|
+
const isRegistered = registry[nestedKey] !== void 0;
|
|
21
|
+
const nextKey = key === "base" ? nestedKey : isRegistered ? `${key}:${nestedKey}` : key;
|
|
22
|
+
return process(nextKey, nestedValue);
|
|
22
23
|
}).join(" ");
|
|
23
24
|
}
|
|
24
|
-
const
|
|
25
|
+
const resolvedPrefix = key.split(":").map((part) => {
|
|
26
|
+
if (part === "base") return null;
|
|
27
|
+
if (registry[part]) return registry[part];
|
|
28
|
+
return null;
|
|
29
|
+
}).filter(Boolean).join(":");
|
|
25
30
|
if (typeof value === "string") {
|
|
26
|
-
return value.split(/[,\s\n]+/).filter(Boolean).map((cls) =>
|
|
31
|
+
return value.split(/[,\s\n]+/).filter(Boolean).map((cls) => !resolvedPrefix ? cls : `${resolvedPrefix}:${cls}`).join(" ");
|
|
27
32
|
}
|
|
28
33
|
return "";
|
|
29
34
|
};
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["twMerge","clsx"],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["twMerge","clsx"],"mappings":";;;;;;;;;;AASO,SAAS,YAAuD,OAAA,EAAmB;AAMtF,EAAA,MAAM,QAAA,GAAmC,OAAO,MAAA,CAAO,EAAE,MAAM,MAAA,EAAO,EAAG,GAAG,OAAO,CAAA;AAQnF,EAAA,MAAM,OAAA,GAAU,CAAC,GAAA,EAAa,KAAA,KAAuB;AACjD,IAAA,IAAI,CAAC,OAAO,OAAO,EAAA;AAGnB,IAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACtB,MAAA,OAAO,KAAA,CACF,GAAA,CAAI,CAAC,CAAA,KAAM,OAAA,CAAQ,GAAA,EAAK,CAAC,CAAC,CAAA,CAC1B,MAAA,CAAO,OAAO,CAAA,CACd,KAAK,GAAG,CAAA;AAAA,IACjB;AAGA,IAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC3B,MAAA,OAAO,MAAA,CAAO,QAAQ,KAAK,CAAA,CACtB,IAAI,CAAC,CAAC,SAAA,EAAW,WAAW,CAAA,KAAM;AAM/B,QAAA,MAAM,YAAA,GAAe,QAAA,CAAS,SAAS,CAAA,KAAM,MAAA;AAC7C,QAAA,MAAM,OAAA,GACF,QAAQ,MAAA,GAAS,SAAA,GAAY,eAAe,CAAA,EAAG,GAAG,CAAA,CAAA,EAAI,SAAS,CAAA,CAAA,GAAK,GAAA;AAExE,QAAA,OAAO,OAAA,CAAQ,SAAS,WAAW,CAAA;AAAA,MACvC,CAAC,CAAA,CACA,IAAA,CAAK,GAAG,CAAA;AAAA,IACjB;AAOA,IAAA,MAAM,iBAAiB,GAAA,CAClB,KAAA,CAAM,GAAG,CAAA,CACT,GAAA,CAAI,CAAC,IAAA,KAAS;AACX,MAAA,IAAI,IAAA,KAAS,QAAQ,OAAO,IAAA;AAE5B,MAAA,IAAI,QAAA,CAAS,IAAI,CAAA,EAAG,OAAO,SAAS,IAAI,CAAA;AAExC,MAAA,OAAO,IAAA;AAAA,IACX,CAAC,CAAA,CACA,MAAA,CAAO,OAAO,CAAA,CACd,KAAK,GAAG,CAAA;AAGb,IAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC3B,MAAA,OAAO,KAAA,CACF,MAAM,UAAU,CAAA,CAChB,OAAO,OAAO,CAAA,CACd,IAAI,CAAC,GAAA,KAAS,CAAC,cAAA,GAAiB,GAAA,GAAM,GAAG,cAAc,CAAA,CAAA,EAAI,GAAG,CAAA,CAAG,CAAA,CACjE,KAAK,GAAG,CAAA;AAAA,IACjB;AACA,IAAA,OAAO,EAAA;AAAA,EACX,CAAA;AAQA,EAAA,OAAO,IAAI,MAAA,KAAkB;AACzB,IAAA,MAAM,SAAA,GAAY,MAAA,CAAO,GAAA,CAAI,CAAC,KAAA,KAAU;AACpC,MAAA,IAAI,KAAA,KAAU,QAAQ,OAAO,KAAA,KAAU,YAAY,CAAC,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACtE,QAAA,OAAO,OAAO,OAAA,CAAQ,KAAK,EACtB,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,CAAC,MAAO,CAAA,KAAM,IAAA,GAAO,IAAI,OAAA,CAAQ,CAAA,EAAG,CAAC,CAAE,CAAA,CAChD,KAAK,GAAG,CAAA;AAAA,MACjB;AACA,MAAA,OAAO,KAAA;AAAA,IACX,CAAC,CAAA;AACD,IAAA,OAAOA,qBAAA,CAAQC,qBAAA,CAAK,SAAS,CAAC,CAAA;AAAA,EAClC,CAAA;AACJ","file":"index.cjs","sourcesContent":["import { twMerge } from 'tailwind-merge';\r\nimport clsx from 'clsx';\r\n\r\n/**\r\n * Creates a specialized utility for managing CSS classes with prefix support,\r\n * plugin mapping, and transparent logical nesting.\r\n * * @param plugins - An array of objects mapping custom aliases to real CSS prefixes.\r\n * @returns A function that processes class values, objects, and nested structures.\r\n */\r\nexport function createCl<TPlugins extends Record<string, string>[]>(...plugins: TPlugins) {\r\n /**\r\n * Internal registry that stores all official prefixes.\r\n * Any key not found here will be treated as a \"transparent\" logical container\r\n * and will be discarded in the final string resolution.\r\n */\r\n const registry: Record<string, string> = Object.assign({ base: 'base' }, ...plugins);\r\n\r\n /**\r\n * Recursively processes keys and values to build the prefixed class string.\r\n * * @param key - The current accumulated prefix path.\r\n * @param value - The class value, array, or nested object to process.\r\n * @returns A space-separated string of prefixed classes.\r\n */\r\n const process = (key: string, value: any): string => {\r\n if (!value) return '';\r\n\r\n // Handle Arrays: Process each element with the current key\r\n if (Array.isArray(value)) {\r\n return value\r\n .map((v) => process(key, v))\r\n .filter(Boolean)\r\n .join(' ');\r\n }\r\n\r\n // Handle Objects: Manage nesting and logical transparency\r\n if (typeof value === 'object') {\r\n return Object.entries(value)\r\n .map(([nestedKey, nestedValue]) => {\r\n /**\r\n * Rule: If the child key is registered, we concatenate it.\r\n * If it's not registered, it's a \"logical\" key (transparent),\r\n * so we inherit the parent's prefix to keep the path clean.\r\n */\r\n const isRegistered = registry[nestedKey] !== undefined;\r\n const nextKey =\r\n key === 'base' ? nestedKey : isRegistered ? `${key}:${nestedKey}` : key;\r\n\r\n return process(nextKey, nestedValue);\r\n })\r\n .join(' ');\r\n }\r\n\r\n /**\r\n * FINAL RESOLUTION\r\n * Maps aliases (e.g., 'ui' -> 'prefix') and filters out any part\r\n * of the path that is not explicitly registered in the registry.\r\n */\r\n const resolvedPrefix = key\r\n .split(':')\r\n .map((part) => {\r\n if (part === 'base') return null;\r\n // Only return the part if it's found in our registry\r\n if (registry[part]) return registry[part];\r\n // Otherwise, it's a logical container and should be ignored\r\n return null;\r\n })\r\n .filter(Boolean)\r\n .join(':');\r\n\r\n // Apply the resolved prefix to each class in the string\r\n if (typeof value === 'string') {\r\n return value\r\n .split(/[,\\s\\n]+/)\r\n .filter(Boolean)\r\n .map((cls) => (!resolvedPrefix ? cls : `${resolvedPrefix}:${cls}`))\r\n .join(' ');\r\n }\r\n return '';\r\n };\r\n\r\n /**\r\n * The final utility function.\r\n * Processes inputs through the prefix engine and cleans them using tailwind-merge.\r\n * * @param inputs - Variadic arguments including strings, objects, arrays, or booleans.\r\n * @returns A merged and optimized string of Tailwind CSS classes.\r\n */\r\n return (...inputs: any[]) => {\r\n const processed = inputs.map((input) => {\r\n if (input !== null && typeof input === 'object' && !Array.isArray(input)) {\r\n return Object.entries(input)\r\n .map(([k, v]) => (v === true ? k : process(k, v)))\r\n .join(' ');\r\n }\r\n return input;\r\n });\r\n return twMerge(clsx(processed));\r\n };\r\n}\r\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
import { ClassValue } from 'clsx';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Helper type to merge an array of objects into one single type.
|
|
5
|
-
* This provides the user with full autocomplete for ALL combined plugins.
|
|
6
|
-
*/
|
|
7
|
-
type MergePlugins<T extends Record<string, string>[]> = T extends [infer First, ...infer Rest] ? First & (Rest extends Record<string, string>[] ? MergePlugins<Rest> : {}) : {};
|
|
8
1
|
/**
|
|
9
|
-
* Creates a
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* @returns A specialized `cl` function with autocompletion for the provided plugins.
|
|
14
|
-
* * @example
|
|
15
|
-
* const myCl = createCl({ ui: 'prefix' });
|
|
16
|
-
* // Autocomplete will suggest 'ui' or 'base'
|
|
17
|
-
* myCl({ ui: { primary: true } }); // 'prefix:primary'
|
|
2
|
+
* Creates a specialized utility for managing CSS classes with prefix support,
|
|
3
|
+
* plugin mapping, and transparent logical nesting.
|
|
4
|
+
* * @param plugins - An array of objects mapping custom aliases to real CSS prefixes.
|
|
5
|
+
* @returns A function that processes class values, objects, and nested structures.
|
|
18
6
|
*/
|
|
19
|
-
declare function createCl<TPlugins extends Record<string, string>[]>(...plugins: TPlugins): (...inputs:
|
|
7
|
+
declare function createCl<TPlugins extends Record<string, string>[]>(...plugins: TPlugins): (...inputs: any[]) => string;
|
|
20
8
|
|
|
21
9
|
export { createCl };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
import { ClassValue } from 'clsx';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Helper type to merge an array of objects into one single type.
|
|
5
|
-
* This provides the user with full autocomplete for ALL combined plugins.
|
|
6
|
-
*/
|
|
7
|
-
type MergePlugins<T extends Record<string, string>[]> = T extends [infer First, ...infer Rest] ? First & (Rest extends Record<string, string>[] ? MergePlugins<Rest> : {}) : {};
|
|
8
1
|
/**
|
|
9
|
-
* Creates a
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* @returns A specialized `cl` function with autocompletion for the provided plugins.
|
|
14
|
-
* * @example
|
|
15
|
-
* const myCl = createCl({ ui: 'prefix' });
|
|
16
|
-
* // Autocomplete will suggest 'ui' or 'base'
|
|
17
|
-
* myCl({ ui: { primary: true } }); // 'prefix:primary'
|
|
2
|
+
* Creates a specialized utility for managing CSS classes with prefix support,
|
|
3
|
+
* plugin mapping, and transparent logical nesting.
|
|
4
|
+
* * @param plugins - An array of objects mapping custom aliases to real CSS prefixes.
|
|
5
|
+
* @returns A function that processes class values, objects, and nested structures.
|
|
18
6
|
*/
|
|
19
|
-
declare function createCl<TPlugins extends Record<string, string>[]>(...plugins: TPlugins): (...inputs:
|
|
7
|
+
declare function createCl<TPlugins extends Record<string, string>[]>(...plugins: TPlugins): (...inputs: any[]) => string;
|
|
20
8
|
|
|
21
9
|
export { createCl };
|
package/dist/index.js
CHANGED
|
@@ -11,13 +11,18 @@ function createCl(...plugins) {
|
|
|
11
11
|
}
|
|
12
12
|
if (typeof value === "object") {
|
|
13
13
|
return Object.entries(value).map(([nestedKey, nestedValue]) => {
|
|
14
|
-
const
|
|
15
|
-
|
|
14
|
+
const isRegistered = registry[nestedKey] !== void 0;
|
|
15
|
+
const nextKey = key === "base" ? nestedKey : isRegistered ? `${key}:${nestedKey}` : key;
|
|
16
|
+
return process(nextKey, nestedValue);
|
|
16
17
|
}).join(" ");
|
|
17
18
|
}
|
|
18
|
-
const
|
|
19
|
+
const resolvedPrefix = key.split(":").map((part) => {
|
|
20
|
+
if (part === "base") return null;
|
|
21
|
+
if (registry[part]) return registry[part];
|
|
22
|
+
return null;
|
|
23
|
+
}).filter(Boolean).join(":");
|
|
19
24
|
if (typeof value === "string") {
|
|
20
|
-
return value.split(/[,\s\n]+/).filter(Boolean).map((cls) =>
|
|
25
|
+
return value.split(/[,\s\n]+/).filter(Boolean).map((cls) => !resolvedPrefix ? cls : `${resolvedPrefix}:${cls}`).join(" ");
|
|
21
26
|
}
|
|
22
27
|
return "";
|
|
23
28
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;;
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;;AASO,SAAS,YAAuD,OAAA,EAAmB;AAMtF,EAAA,MAAM,QAAA,GAAmC,OAAO,MAAA,CAAO,EAAE,MAAM,MAAA,EAAO,EAAG,GAAG,OAAO,CAAA;AAQnF,EAAA,MAAM,OAAA,GAAU,CAAC,GAAA,EAAa,KAAA,KAAuB;AACjD,IAAA,IAAI,CAAC,OAAO,OAAO,EAAA;AAGnB,IAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACtB,MAAA,OAAO,KAAA,CACF,GAAA,CAAI,CAAC,CAAA,KAAM,OAAA,CAAQ,GAAA,EAAK,CAAC,CAAC,CAAA,CAC1B,MAAA,CAAO,OAAO,CAAA,CACd,KAAK,GAAG,CAAA;AAAA,IACjB;AAGA,IAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC3B,MAAA,OAAO,MAAA,CAAO,QAAQ,KAAK,CAAA,CACtB,IAAI,CAAC,CAAC,SAAA,EAAW,WAAW,CAAA,KAAM;AAM/B,QAAA,MAAM,YAAA,GAAe,QAAA,CAAS,SAAS,CAAA,KAAM,MAAA;AAC7C,QAAA,MAAM,OAAA,GACF,QAAQ,MAAA,GAAS,SAAA,GAAY,eAAe,CAAA,EAAG,GAAG,CAAA,CAAA,EAAI,SAAS,CAAA,CAAA,GAAK,GAAA;AAExE,QAAA,OAAO,OAAA,CAAQ,SAAS,WAAW,CAAA;AAAA,MACvC,CAAC,CAAA,CACA,IAAA,CAAK,GAAG,CAAA;AAAA,IACjB;AAOA,IAAA,MAAM,iBAAiB,GAAA,CAClB,KAAA,CAAM,GAAG,CAAA,CACT,GAAA,CAAI,CAAC,IAAA,KAAS;AACX,MAAA,IAAI,IAAA,KAAS,QAAQ,OAAO,IAAA;AAE5B,MAAA,IAAI,QAAA,CAAS,IAAI,CAAA,EAAG,OAAO,SAAS,IAAI,CAAA;AAExC,MAAA,OAAO,IAAA;AAAA,IACX,CAAC,CAAA,CACA,MAAA,CAAO,OAAO,CAAA,CACd,KAAK,GAAG,CAAA;AAGb,IAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC3B,MAAA,OAAO,KAAA,CACF,MAAM,UAAU,CAAA,CAChB,OAAO,OAAO,CAAA,CACd,IAAI,CAAC,GAAA,KAAS,CAAC,cAAA,GAAiB,GAAA,GAAM,GAAG,cAAc,CAAA,CAAA,EAAI,GAAG,CAAA,CAAG,CAAA,CACjE,KAAK,GAAG,CAAA;AAAA,IACjB;AACA,IAAA,OAAO,EAAA;AAAA,EACX,CAAA;AAQA,EAAA,OAAO,IAAI,MAAA,KAAkB;AACzB,IAAA,MAAM,SAAA,GAAY,MAAA,CAAO,GAAA,CAAI,CAAC,KAAA,KAAU;AACpC,MAAA,IAAI,KAAA,KAAU,QAAQ,OAAO,KAAA,KAAU,YAAY,CAAC,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACtE,QAAA,OAAO,OAAO,OAAA,CAAQ,KAAK,EACtB,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,CAAC,MAAO,CAAA,KAAM,IAAA,GAAO,IAAI,OAAA,CAAQ,CAAA,EAAG,CAAC,CAAE,CAAA,CAChD,KAAK,GAAG,CAAA;AAAA,MACjB;AACA,MAAA,OAAO,KAAA;AAAA,IACX,CAAC,CAAA;AACD,IAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,SAAS,CAAC,CAAA;AAAA,EAClC,CAAA;AACJ","file":"index.js","sourcesContent":["import { twMerge } from 'tailwind-merge';\r\nimport clsx from 'clsx';\r\n\r\n/**\r\n * Creates a specialized utility for managing CSS classes with prefix support,\r\n * plugin mapping, and transparent logical nesting.\r\n * * @param plugins - An array of objects mapping custom aliases to real CSS prefixes.\r\n * @returns A function that processes class values, objects, and nested structures.\r\n */\r\nexport function createCl<TPlugins extends Record<string, string>[]>(...plugins: TPlugins) {\r\n /**\r\n * Internal registry that stores all official prefixes.\r\n * Any key not found here will be treated as a \"transparent\" logical container\r\n * and will be discarded in the final string resolution.\r\n */\r\n const registry: Record<string, string> = Object.assign({ base: 'base' }, ...plugins);\r\n\r\n /**\r\n * Recursively processes keys and values to build the prefixed class string.\r\n * * @param key - The current accumulated prefix path.\r\n * @param value - The class value, array, or nested object to process.\r\n * @returns A space-separated string of prefixed classes.\r\n */\r\n const process = (key: string, value: any): string => {\r\n if (!value) return '';\r\n\r\n // Handle Arrays: Process each element with the current key\r\n if (Array.isArray(value)) {\r\n return value\r\n .map((v) => process(key, v))\r\n .filter(Boolean)\r\n .join(' ');\r\n }\r\n\r\n // Handle Objects: Manage nesting and logical transparency\r\n if (typeof value === 'object') {\r\n return Object.entries(value)\r\n .map(([nestedKey, nestedValue]) => {\r\n /**\r\n * Rule: If the child key is registered, we concatenate it.\r\n * If it's not registered, it's a \"logical\" key (transparent),\r\n * so we inherit the parent's prefix to keep the path clean.\r\n */\r\n const isRegistered = registry[nestedKey] !== undefined;\r\n const nextKey =\r\n key === 'base' ? nestedKey : isRegistered ? `${key}:${nestedKey}` : key;\r\n\r\n return process(nextKey, nestedValue);\r\n })\r\n .join(' ');\r\n }\r\n\r\n /**\r\n * FINAL RESOLUTION\r\n * Maps aliases (e.g., 'ui' -> 'prefix') and filters out any part\r\n * of the path that is not explicitly registered in the registry.\r\n */\r\n const resolvedPrefix = key\r\n .split(':')\r\n .map((part) => {\r\n if (part === 'base') return null;\r\n // Only return the part if it's found in our registry\r\n if (registry[part]) return registry[part];\r\n // Otherwise, it's a logical container and should be ignored\r\n return null;\r\n })\r\n .filter(Boolean)\r\n .join(':');\r\n\r\n // Apply the resolved prefix to each class in the string\r\n if (typeof value === 'string') {\r\n return value\r\n .split(/[,\\s\\n]+/)\r\n .filter(Boolean)\r\n .map((cls) => (!resolvedPrefix ? cls : `${resolvedPrefix}:${cls}`))\r\n .join(' ');\r\n }\r\n return '';\r\n };\r\n\r\n /**\r\n * The final utility function.\r\n * Processes inputs through the prefix engine and cleans them using tailwind-merge.\r\n * * @param inputs - Variadic arguments including strings, objects, arrays, or booleans.\r\n * @returns A merged and optimized string of Tailwind CSS classes.\r\n */\r\n return (...inputs: any[]) => {\r\n const processed = inputs.map((input) => {\r\n if (input !== null && typeof input === 'object' && !Array.isArray(input)) {\r\n return Object.entries(input)\r\n .map(([k, v]) => (v === true ? k : process(k, v)))\r\n .join(' ');\r\n }\r\n return input;\r\n });\r\n return twMerge(clsx(processed));\r\n };\r\n}\r\n"]}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,37 +1,30 @@
|
|
|
1
1
|
import { twMerge } from 'tailwind-merge';
|
|
2
|
-
import clsx
|
|
2
|
+
import clsx from 'clsx';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
? First & (Rest extends Record<string, string>[] ? MergePlugins<Rest> : {})
|
|
10
|
-
: {};
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Creates a customized class utility instance with plugin support.
|
|
14
|
-
* * This factory function merges multiple prefix plugins and returns a scoped
|
|
15
|
-
* `cl` function that handles recursive prefixing, tailwind-merge, and clsx logic.
|
|
16
|
-
* * @param plugins - One or more objects defining prefix maps (e.g., { btn: 'button' }).
|
|
17
|
-
* @returns A specialized `cl` function with autocompletion for the provided plugins.
|
|
18
|
-
* * @example
|
|
19
|
-
* const myCl = createCl({ ui: 'prefix' });
|
|
20
|
-
* // Autocomplete will suggest 'ui' or 'base'
|
|
21
|
-
* myCl({ ui: { primary: true } }); // 'prefix:primary'
|
|
5
|
+
* Creates a specialized utility for managing CSS classes with prefix support,
|
|
6
|
+
* plugin mapping, and transparent logical nesting.
|
|
7
|
+
* * @param plugins - An array of objects mapping custom aliases to real CSS prefixes.
|
|
8
|
+
* @returns A function that processes class values, objects, and nested structures.
|
|
22
9
|
*/
|
|
23
10
|
export function createCl<TPlugins extends Record<string, string>[]>(...plugins: TPlugins) {
|
|
24
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Internal registry that stores all official prefixes.
|
|
13
|
+
* Any key not found here will be treated as a "transparent" logical container
|
|
14
|
+
* and will be discarded in the final string resolution.
|
|
15
|
+
*/
|
|
25
16
|
const registry: Record<string, string> = Object.assign({ base: 'base' }, ...plugins);
|
|
26
17
|
|
|
27
|
-
type CombinedKeys = keyof MergePlugins<TPlugins>;
|
|
28
|
-
|
|
29
18
|
/**
|
|
30
|
-
*
|
|
19
|
+
* Recursively processes keys and values to build the prefixed class string.
|
|
20
|
+
* * @param key - The current accumulated prefix path.
|
|
21
|
+
* @param value - The class value, array, or nested object to process.
|
|
22
|
+
* @returns A space-separated string of prefixed classes.
|
|
31
23
|
*/
|
|
32
24
|
const process = (key: string, value: any): string => {
|
|
33
25
|
if (!value) return '';
|
|
34
26
|
|
|
27
|
+
// Handle Arrays: Process each element with the current key
|
|
35
28
|
if (Array.isArray(value)) {
|
|
36
29
|
return value
|
|
37
30
|
.map((v) => process(key, v))
|
|
@@ -39,42 +32,59 @@ export function createCl<TPlugins extends Record<string, string>[]>(...plugins:
|
|
|
39
32
|
.join(' ');
|
|
40
33
|
}
|
|
41
34
|
|
|
35
|
+
// Handle Objects: Manage nesting and logical transparency
|
|
42
36
|
if (typeof value === 'object') {
|
|
43
37
|
return Object.entries(value)
|
|
44
38
|
.map(([nestedKey, nestedValue]) => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Rule: If the child key is registered, we concatenate it.
|
|
41
|
+
* If it's not registered, it's a "logical" key (transparent),
|
|
42
|
+
* so we inherit the parent's prefix to keep the path clean.
|
|
43
|
+
*/
|
|
44
|
+
const isRegistered = registry[nestedKey] !== undefined;
|
|
45
|
+
const nextKey =
|
|
46
|
+
key === 'base' ? nestedKey : isRegistered ? `${key}:${nestedKey}` : key;
|
|
52
47
|
|
|
53
|
-
return process(
|
|
48
|
+
return process(nextKey, nestedValue);
|
|
54
49
|
})
|
|
55
50
|
.join(' ');
|
|
56
51
|
}
|
|
57
52
|
|
|
58
|
-
|
|
53
|
+
/**
|
|
54
|
+
* FINAL RESOLUTION
|
|
55
|
+
* Maps aliases (e.g., 'ui' -> 'prefix') and filters out any part
|
|
56
|
+
* of the path that is not explicitly registered in the registry.
|
|
57
|
+
*/
|
|
58
|
+
const resolvedPrefix = key
|
|
59
|
+
.split(':')
|
|
60
|
+
.map((part) => {
|
|
61
|
+
if (part === 'base') return null;
|
|
62
|
+
// Only return the part if it's found in our registry
|
|
63
|
+
if (registry[part]) return registry[part];
|
|
64
|
+
// Otherwise, it's a logical container and should be ignored
|
|
65
|
+
return null;
|
|
66
|
+
})
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.join(':');
|
|
59
69
|
|
|
70
|
+
// Apply the resolved prefix to each class in the string
|
|
60
71
|
if (typeof value === 'string') {
|
|
61
72
|
return value
|
|
62
73
|
.split(/[,\s\n]+/)
|
|
63
74
|
.filter(Boolean)
|
|
64
|
-
.map((cls) => (
|
|
75
|
+
.map((cls) => (!resolvedPrefix ? cls : `${resolvedPrefix}:${cls}`))
|
|
65
76
|
.join(' ');
|
|
66
77
|
}
|
|
67
78
|
return '';
|
|
68
79
|
};
|
|
69
80
|
|
|
70
81
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* @returns A merged string of CSS classes.
|
|
82
|
+
* The final utility function.
|
|
83
|
+
* Processes inputs through the prefix engine and cleans them using tailwind-merge.
|
|
84
|
+
* * @param inputs - Variadic arguments including strings, objects, arrays, or booleans.
|
|
85
|
+
* @returns A merged and optimized string of Tailwind CSS classes.
|
|
76
86
|
*/
|
|
77
|
-
return (...inputs:
|
|
87
|
+
return (...inputs: any[]) => {
|
|
78
88
|
const processed = inputs.map((input) => {
|
|
79
89
|
if (input !== null && typeof input === 'object' && !Array.isArray(input)) {
|
|
80
90
|
return Object.entries(input)
|
package/src/test/cn.test.ts
CHANGED
|
@@ -1,60 +1,125 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { createCl } from '../index';
|
|
3
3
|
|
|
4
|
-
describe('
|
|
5
|
-
// 1.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
describe('cl - Escala de Complejidad (Registry-Based)', () => {
|
|
5
|
+
// 1. Setup con Prefijos y Plugins registrados
|
|
6
|
+
const cl = createCl(
|
|
7
|
+
{ md: 'md', hover: 'hover', focus: 'focus' }, // Tailwind
|
|
8
|
+
{ ui: 'prefix', btn: 'button' } // Plugins
|
|
9
|
+
);
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
it('Nivel 1: Uso básico (Strings y Arrays)', () => {
|
|
12
|
+
expect(cl('p-4', ['m-2', 'flex'])).toBe('p-4 m-2 flex');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('Nivel 2: Objetos Simples y Registro', () => {
|
|
16
|
+
// 'md' está registrado, 'p-4' es el valor
|
|
17
|
+
expect(cl({ md: 'p-4' })).toBe('md:p-4');
|
|
18
|
+
// 'ui' se mapea a 'prefix'
|
|
19
|
+
expect(cl({ ui: 'p-4' })).toBe('prefix:p-4');
|
|
20
|
+
});
|
|
11
21
|
|
|
22
|
+
it('Nivel 3: Transparencia Automática (Sintaxis Hermosa)', () => {
|
|
23
|
+
// 'custom' NO está registrado -> Invisible
|
|
24
|
+
// 'inner' NO está registrado -> Invisible
|
|
12
25
|
const result = cl({
|
|
13
|
-
|
|
14
|
-
|
|
26
|
+
custom: {
|
|
27
|
+
inner: 'text-center'
|
|
28
|
+
}
|
|
15
29
|
});
|
|
16
|
-
|
|
17
|
-
expect(result).toBe('md:p-4 hover:focus:scale-110');
|
|
30
|
+
expect(result).toBe('text-center');
|
|
18
31
|
});
|
|
19
32
|
|
|
20
|
-
|
|
21
|
-
it('should stack prefixes when objects are nested', () => {
|
|
22
|
-
const cl = createCl({ md: 'md', hover: 'hover', dark: 'dark' });
|
|
23
|
-
|
|
33
|
+
it('Nivel 4: Nesting Híbrido (Registrado + Transparente)', () => {
|
|
24
34
|
const result = cl({
|
|
25
35
|
md: {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
// Registrado
|
|
37
|
+
card: {
|
|
38
|
+
// Lógico (Transparente)
|
|
39
|
+
hover: {
|
|
40
|
+
// Registrado
|
|
41
|
+
base: 'shadow-lg'
|
|
42
|
+
}
|
|
31
43
|
}
|
|
32
44
|
}
|
|
33
45
|
});
|
|
46
|
+
// Debe saltar 'card' y conectar md con hover
|
|
47
|
+
expect(result).toBe('md:hover:shadow-lg');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('Nivel 5: El "Inception" (Lógica anidada, condicionales y plugins)', () => {
|
|
51
|
+
const isEnabled = true;
|
|
52
|
+
const variant = 'primary';
|
|
53
|
+
const theme = 'dark';
|
|
54
|
+
|
|
55
|
+
const result = cl({
|
|
56
|
+
md: {
|
|
57
|
+
[isEnabled ? 'active' : 'idle']: {
|
|
58
|
+
ui: {
|
|
59
|
+
[`[${variant}]`]: {
|
|
60
|
+
base: 'bg-blue-500',
|
|
61
|
+
// Seleccionamos el valor ANTES de que el motor lo aplane
|
|
62
|
+
theme: theme === 'dark' ? 'text-white' : 'text-black'
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
34
68
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
69
|
+
expect(result).toContain('md:prefix:bg-blue-500');
|
|
70
|
+
expect(result).toContain('md:prefix:text-white'); // Ahora sí pasará
|
|
71
|
+
expect(result).not.toContain('md:prefix:text-black');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('Nivel 6: Resolución de Conflictos Final (twMerge)', () => {
|
|
75
|
+
// En este nivel probamos que tras toda la recursión, twMerge limpie el resultado
|
|
76
|
+
const result = cl({ md: 'p-4' }, { md: { logic: 'p-8' } });
|
|
77
|
+
// md:p-8 debe ganar a md:p-4
|
|
78
|
+
expect(result).toBe('md:p-8');
|
|
39
79
|
});
|
|
40
80
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
81
|
+
it('Nivel 7: Selectores Dinámicos (Sintaxis de Componente)', () => {
|
|
82
|
+
const variant = 'primary';
|
|
83
|
+
const onHover = 'scale';
|
|
44
84
|
|
|
45
85
|
const result = cl({
|
|
46
|
-
|
|
86
|
+
// 'variants' no está registrado -> Desaparece
|
|
87
|
+
variants: {
|
|
88
|
+
primary: 'bg-action text-white',
|
|
89
|
+
secondary: 'bg-neutral-50 text-black'
|
|
90
|
+
}[variant],
|
|
91
|
+
|
|
92
|
+
// 'effects' no está registrado -> Desaparece
|
|
93
|
+
effects: {
|
|
94
|
+
scale: 'elevate-hover',
|
|
95
|
+
dim: 'hover:opacity-80'
|
|
96
|
+
}[onHover]
|
|
47
97
|
});
|
|
48
98
|
|
|
49
|
-
|
|
99
|
+
// El resultado no debe contener ni 'variants:' ni 'effects:'
|
|
100
|
+
expect(result).toBe('bg-action text-white elevate-hover');
|
|
101
|
+
expect(result).not.toContain('variants:');
|
|
102
|
+
expect(result).not.toContain('effects:');
|
|
50
103
|
});
|
|
51
104
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
105
|
+
it('Nivel 8: Combinación de Selectores con Prefijos Registrados', () => {
|
|
106
|
+
const variant = 'secondary';
|
|
107
|
+
const isDark = true;
|
|
108
|
+
|
|
109
|
+
const result = cl({
|
|
110
|
+
md: {
|
|
111
|
+
// Selector dinámico dentro de un prefijo registrado
|
|
112
|
+
container: {
|
|
113
|
+
[variant]: {
|
|
114
|
+
base: 'p-4',
|
|
115
|
+
// Prefijo registrado dentro de un selector dinámico
|
|
116
|
+
hover: 'shadow-xl'
|
|
117
|
+
}
|
|
118
|
+
}[variant]
|
|
119
|
+
}
|
|
120
|
+
});
|
|
55
121
|
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
expect(result).toBe('p-8');
|
|
122
|
+
// 'md' y 'hover' se mantienen, 'container' y 'secondary' desaparecen
|
|
123
|
+
expect(result).toBe('md:p-4 md:hover:shadow-xl');
|
|
59
124
|
});
|
|
60
125
|
});
|