clases 1.0.1 → 1.1.0
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 -6
- package/dist/index.d.ts +5 -6
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +57 -27
- package/src/test/cn.test.ts +58 -38
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;AAKtF,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 \"transparent\" logic.\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.\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') to real prefixes (e.g., 'prefix')\r\n * and removes any non-registered logical parts.\r\n */\r\n const resolvedPrefix = key\r\n .split(':')\r\n .map((part) => {\r\n if (part === 'base') return null;\r\n // Return mapped value from registry if it exists\r\n if (registry[part]) return registry[part];\r\n // Otherwise, discard the part (Total Transparency)\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,10 +1,9 @@
|
|
|
1
|
-
import { ClassValue } from 'clsx';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
declare function createCl<TPlugins extends Record<string, string>[]>(...plugins: TPlugins): (...inputs: (ClassValue | { [K in keyof MergePlugins<TPlugins> | "base" | (string & {})]?: any; })[]) => string;
|
|
7
|
+
declare function createCl<TPlugins extends Record<string, string>[]>(...plugins: TPlugins): (...inputs: any[]) => string;
|
|
9
8
|
|
|
10
9
|
export { createCl };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { ClassValue } from 'clsx';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
declare function createCl<TPlugins extends Record<string, string>[]>(...plugins: TPlugins): (...inputs: (ClassValue | { [K in keyof MergePlugins<TPlugins> | "base" | (string & {})]?: any; })[]) => string;
|
|
7
|
+
declare function createCl<TPlugins extends Record<string, string>[]>(...plugins: TPlugins): (...inputs: any[]) => string;
|
|
9
8
|
|
|
10
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;AAKtF,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 \"transparent\" logic.\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.\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') to real prefixes (e.g., 'prefix')\r\n * and removes any non-registered logical parts.\r\n */\r\n const resolvedPrefix = key\r\n .split(':')\r\n .map((part) => {\r\n if (part === 'base') return null;\r\n // Return mapped value from registry if it exists\r\n if (registry[part]) return registry[part];\r\n // Otherwise, discard the part (Total Transparency)\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,23 +1,29 @@
|
|
|
1
1
|
import { twMerge } from 'tailwind-merge';
|
|
2
|
-
import clsx
|
|
2
|
+
import clsx from 'clsx';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|
|
7
9
|
*/
|
|
8
|
-
type MergePlugins<T extends Record<string, string>[]> = T extends [infer First, ...infer Rest]
|
|
9
|
-
? First & (Rest extends Record<string, string>[] ? MergePlugins<Rest> : {})
|
|
10
|
-
: {};
|
|
11
|
-
|
|
12
10
|
export function createCl<TPlugins extends Record<string, string>[]>(...plugins: TPlugins) {
|
|
13
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Internal registry that stores all official prefixes.
|
|
13
|
+
* Any key not found here will be treated as "transparent" logic.
|
|
14
|
+
*/
|
|
14
15
|
const registry: Record<string, string> = Object.assign({ base: 'base' }, ...plugins);
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Recursively processes keys and values to build the prefixed class string.
|
|
19
|
+
* * @param key - The current accumulated prefix path.
|
|
20
|
+
* @param value - The class value, array, or nested object to process.
|
|
21
|
+
* @returns A space-separated string of prefixed classes.
|
|
22
|
+
*/
|
|
18
23
|
const process = (key: string, value: any): string => {
|
|
19
24
|
if (!value) return '';
|
|
20
25
|
|
|
26
|
+
// Handle Arrays: Process each element with the current key
|
|
21
27
|
if (Array.isArray(value)) {
|
|
22
28
|
return value
|
|
23
29
|
.map((v) => process(key, v))
|
|
@@ -25,35 +31,59 @@ export function createCl<TPlugins extends Record<string, string>[]>(...plugins:
|
|
|
25
31
|
.join(' ');
|
|
26
32
|
}
|
|
27
33
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
// Handle Objects: Manage nesting and logical transparency
|
|
35
|
+
if (typeof value === 'object') {
|
|
36
|
+
return Object.entries(value)
|
|
37
|
+
.map(([nestedKey, nestedValue]) => {
|
|
38
|
+
/**
|
|
39
|
+
* Rule: If the child key is registered, we concatenate it.
|
|
40
|
+
* If it's not registered, it's a "logical" key (transparent),
|
|
41
|
+
* so we inherit the parent's prefix.
|
|
42
|
+
*/
|
|
43
|
+
const isRegistered = registry[nestedKey] !== undefined;
|
|
44
|
+
const nextKey =
|
|
45
|
+
key === 'base' ? nestedKey : isRegistered ? `${key}:${nestedKey}` : key;
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
47
|
+
return process(nextKey, nestedValue);
|
|
48
|
+
})
|
|
49
|
+
.join(' ');
|
|
50
|
+
}
|
|
43
51
|
|
|
44
|
-
|
|
52
|
+
/**
|
|
53
|
+
* FINAL RESOLUTION
|
|
54
|
+
* Maps aliases (e.g., 'ui') to real prefixes (e.g., 'prefix')
|
|
55
|
+
* and removes any non-registered logical parts.
|
|
56
|
+
*/
|
|
57
|
+
const resolvedPrefix = key
|
|
58
|
+
.split(':')
|
|
59
|
+
.map((part) => {
|
|
60
|
+
if (part === 'base') return null;
|
|
61
|
+
// Return mapped value from registry if it exists
|
|
62
|
+
if (registry[part]) return registry[part];
|
|
63
|
+
// Otherwise, discard the part (Total Transparency)
|
|
64
|
+
return null;
|
|
65
|
+
})
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.join(':');
|
|
45
68
|
|
|
69
|
+
// Apply the resolved prefix to each class in the string
|
|
46
70
|
if (typeof value === 'string') {
|
|
47
71
|
return value
|
|
48
72
|
.split(/[,\s\n]+/)
|
|
49
73
|
.filter(Boolean)
|
|
50
|
-
.map((cls) => (
|
|
74
|
+
.map((cls) => (!resolvedPrefix ? cls : `${resolvedPrefix}:${cls}`))
|
|
51
75
|
.join(' ');
|
|
52
76
|
}
|
|
53
77
|
return '';
|
|
54
78
|
};
|
|
55
79
|
|
|
56
|
-
|
|
80
|
+
/**
|
|
81
|
+
* The final utility function.
|
|
82
|
+
* Processes inputs through the prefix engine and cleans them using tailwind-merge.
|
|
83
|
+
* * @param inputs - Variadic arguments including strings, objects, arrays, or booleans.
|
|
84
|
+
* @returns A merged and optimized string of Tailwind CSS classes.
|
|
85
|
+
*/
|
|
86
|
+
return (...inputs: any[]) => {
|
|
57
87
|
const processed = inputs.map((input) => {
|
|
58
88
|
if (input !== null && typeof input === 'object' && !Array.isArray(input)) {
|
|
59
89
|
return Object.entries(input)
|
package/src/test/cn.test.ts
CHANGED
|
@@ -1,60 +1,80 @@
|
|
|
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
|
});
|
|
34
|
-
|
|
35
|
-
expect(result).
|
|
36
|
-
expect(result).toContain('md:text-black');
|
|
37
|
-
expect(result).toContain('md:dark:text-white'); // Deep base check
|
|
38
|
-
expect(result).toContain('md:dark:focus:ring-2'); // Deep stack check
|
|
46
|
+
// Debe saltar 'card' y conectar md con hover
|
|
47
|
+
expect(result).toBe('md:hover:shadow-lg');
|
|
39
48
|
});
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
it('Nivel 5: El "Inception" (Lógica anidada, condicionales y plugins)', () => {
|
|
51
|
+
const isEnabled = true;
|
|
52
|
+
const variant = 'primary';
|
|
53
|
+
const theme = 'dark';
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
}
|
|
50
67
|
});
|
|
51
68
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
});
|
|
55
73
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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');
|
|
59
79
|
});
|
|
60
80
|
});
|