clases 1.1.10 → 1.1.11
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 +43 -101
- package/dist/index.cjs +17 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +13 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +51 -28
- package/src/test/cn.test.ts +25 -0
package/README.md
CHANGED
|
@@ -4,19 +4,19 @@ A high-performance, recursive, and type-safe utility for managing CSS classes. D
|
|
|
4
4
|
|
|
5
5
|
## ✨ Features
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
7
|
+
- 🔄 **Deep Recursion:** Nest variants like `md: { hover: '...' }` to stack prefixes automatically.
|
|
8
|
+
- 🛡️ **Hard Typing:** Full IntelliSense autocomplete for all Tailwind variants and custom plugins.
|
|
9
|
+
- 🔌 **Stackable Plugins:** Merge multiple design systems or custom configurations into a single `cl` function.
|
|
10
|
+
- 🗜️ **Zero Overhead:** Built on top of `tailwind-merge` and `clsx` for optimal performance and conflict resolution.
|
|
11
|
+
- 📦 **Monorepo Ready:** Lightweight, tree-shakable packages.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
## 📦 Packages
|
|
16
16
|
|
|
17
|
-
| Package
|
|
18
|
-
|
|
|
19
|
-
| **clases**
|
|
17
|
+
| Package | Description |
|
|
18
|
+
| :------------------ | :------------------------------------------------------------------ |
|
|
19
|
+
| **clases** | The recursive engine. Use this to build custom variants or plugins. |
|
|
20
20
|
| **clases-tailwind** | Pre-configured with all Tailwind CSS variants and type definitions. |
|
|
21
21
|
|
|
22
22
|
---
|
|
@@ -35,13 +35,13 @@ pnpm add clases-tailwind clases
|
|
|
35
35
|
import { cl } from 'clases-tailwind';
|
|
36
36
|
|
|
37
37
|
const className = cl({
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
base: 'p-4 text-sm transition-all',
|
|
39
|
+
hover: 'bg-blue-500 text-white',
|
|
40
|
+
md: 'text-lg p-8',
|
|
41
|
+
dark: {
|
|
42
|
+
base: 'bg-gray-900',
|
|
43
|
+
hover: 'bg-gray-800'
|
|
44
|
+
}
|
|
45
45
|
});
|
|
46
46
|
```
|
|
47
47
|
|
|
@@ -50,144 +50,86 @@ const className = cl({
|
|
|
50
50
|
## 💡 Advanced Use Cases
|
|
51
51
|
|
|
52
52
|
### 🔄 Recursive Stacking (The "Secret Sauce")
|
|
53
|
+
|
|
53
54
|
Stop repeating prefixes. Nesting objects automatically stacks variants in the correct order.
|
|
54
55
|
|
|
55
56
|
```typescript
|
|
56
57
|
cl({
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
md: {
|
|
59
|
+
hover: {
|
|
60
|
+
base: 'scale-105',
|
|
61
|
+
after: 'content-["*"]'
|
|
62
|
+
}
|
|
61
63
|
}
|
|
62
|
-
}
|
|
63
64
|
});
|
|
64
65
|
// Result: "md:hover:scale-105 md:hover:after:content-['*']"
|
|
65
66
|
```
|
|
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
|
|
80
|
-
|
|
81
|
-
This structure allows you to colocate base styles, responsive modifiers, and interaction states within a single logical branch:
|
|
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
67
|
|
|
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
70
|
### 🛠️ Custom Plugin Management
|
|
71
|
+
|
|
133
72
|
You can stack the Tailwind plugin with your own semantic aliases or project-specific configs.
|
|
134
73
|
|
|
135
74
|
```typescript
|
|
136
75
|
import { createCl } from 'clases';
|
|
137
76
|
import { tailwind } from 'clases-tailwind';
|
|
138
77
|
|
|
139
|
-
const cl = createCl(
|
|
140
|
-
tailwind,
|
|
141
|
-
{
|
|
78
|
+
const cl = createCl(tailwind, {
|
|
142
79
|
hocus: 'hover:focus',
|
|
143
|
-
brand: 'text-indigo-600 dark:text-indigo-400'
|
|
144
|
-
|
|
145
|
-
);
|
|
80
|
+
brand: 'text-indigo-600 dark:text-indigo-400'
|
|
81
|
+
});
|
|
146
82
|
|
|
147
|
-
cl({
|
|
148
|
-
|
|
149
|
-
|
|
83
|
+
cl({
|
|
84
|
+
hocus: 'outline-none ring-2',
|
|
85
|
+
brand: 'font-bold'
|
|
150
86
|
});
|
|
151
87
|
```
|
|
152
88
|
|
|
153
89
|
### 📂 Clean Multi-line Layouts
|
|
90
|
+
|
|
154
91
|
Use backticks and commas to organize large chunks of layout logic without losing readability.
|
|
155
92
|
|
|
156
93
|
```typescript
|
|
157
94
|
cl({
|
|
158
|
-
|
|
95
|
+
base: `
|
|
159
96
|
grid grid-cols-1,
|
|
160
97
|
gap-4 items-center,
|
|
161
98
|
w-full max-w-7xl mx-auto
|
|
162
99
|
`,
|
|
163
|
-
|
|
100
|
+
lg: 'grid-cols-3 gap-8'
|
|
164
101
|
});
|
|
165
102
|
```
|
|
103
|
+
|
|
166
104
|
---
|
|
167
105
|
|
|
168
106
|
## ⌨️ Why Objects?
|
|
169
107
|
|
|
170
|
-
| Feature
|
|
171
|
-
|
|
|
172
|
-
| **Readability** | ❌ Hard to scan long lines
|
|
173
|
-
| **Maintenance** | ❌ Easy to forget prefixes
|
|
174
|
-
| **Logic**
|
|
175
|
-
| **Types**
|
|
108
|
+
| Feature | Standard Tailwind Strings | Class Utilities Objects |
|
|
109
|
+
| :-------------- | :-------------------------- | :------------------------ |
|
|
110
|
+
| **Readability** | ❌ Hard to scan long lines | ✅ Grouped by variant |
|
|
111
|
+
| **Maintenance** | ❌ Easy to forget prefixes | ✅ Automatic stacking |
|
|
112
|
+
| **Logic** | ❌ Messy ternary operators | ✅ Native JS object logic |
|
|
113
|
+
| **Types** | ❌ String-based (no safety) | ✅ Full Autocomplete |
|
|
176
114
|
|
|
177
115
|
---
|
|
178
116
|
|
|
179
117
|
## 🛠️ API Reference
|
|
180
118
|
|
|
181
119
|
### `cl(...inputs)`
|
|
120
|
+
|
|
182
121
|
The main utility function. Accepts strings, arrays, objects, or nested structures.
|
|
183
122
|
|
|
184
123
|
### `createCl(...plugins)`
|
|
124
|
+
|
|
185
125
|
Factory function to create a customized `cl` instance. Merges all provided objects into a single type-safe registry.
|
|
186
126
|
|
|
187
127
|
### `tailwind`
|
|
128
|
+
|
|
188
129
|
The raw plugin data containing all Tailwind CSS variants.
|
|
189
130
|
|
|
190
131
|
---
|
|
191
132
|
|
|
192
133
|
## 📄 License
|
|
193
|
-
|
|
134
|
+
|
|
135
|
+
MIT © Mauricio Frías
|
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var tailwindMerge = require('tailwind-merge');
|
|
4
|
+
var clsx = require('clsx');
|
|
5
|
+
|
|
6
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var clsx__default = /*#__PURE__*/_interopDefault(clsx);
|
|
4
9
|
|
|
5
10
|
// src/index.ts
|
|
6
11
|
function createCl(...plugins) {
|
|
@@ -8,30 +13,30 @@ function createCl(...plugins) {
|
|
|
8
13
|
const process = (accumulatedPrefix, input) => {
|
|
9
14
|
if (!input) return "";
|
|
10
15
|
if (typeof input === "string") {
|
|
11
|
-
|
|
16
|
+
const resolved = accumulatedPrefix.split(":").map((part) => part === "base" ? null : registry[part] || null).filter(Boolean).join(":");
|
|
17
|
+
return input.split(/[,\s\n]+/).filter(Boolean).map((cls) => resolved ? `${resolved}:${cls}` : cls).join(" ");
|
|
12
18
|
}
|
|
13
19
|
if (Array.isArray(input)) {
|
|
14
20
|
return input.map((i) => process(accumulatedPrefix, i)).filter(Boolean).join(" ");
|
|
15
21
|
}
|
|
16
22
|
if (typeof input === "object") {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
const nextPrefix = accumulatedPrefix ? `${accumulatedPrefix}:${
|
|
23
|
-
|
|
23
|
+
return Object.entries(input).map(([key, value]) => {
|
|
24
|
+
if (!value) return "";
|
|
25
|
+
const isBase = key === "base";
|
|
26
|
+
const isPrefix = registry[key] !== void 0;
|
|
27
|
+
if (isBase || isPrefix) {
|
|
28
|
+
const nextPrefix = accumulatedPrefix ? `${accumulatedPrefix}:${key}` : key;
|
|
29
|
+
return process(nextPrefix, value);
|
|
24
30
|
} else {
|
|
25
|
-
|
|
31
|
+
return process(accumulatedPrefix, key);
|
|
26
32
|
}
|
|
27
|
-
}
|
|
28
|
-
return results.join(" ");
|
|
33
|
+
}).filter(Boolean).join(" ");
|
|
29
34
|
}
|
|
30
35
|
return "";
|
|
31
36
|
};
|
|
32
37
|
return (...inputs) => {
|
|
33
38
|
const processed = inputs.map((input) => process("", input));
|
|
34
|
-
return tailwindMerge.twMerge(
|
|
39
|
+
return tailwindMerge.twMerge(clsx__default.default(processed));
|
|
35
40
|
};
|
|
36
41
|
}
|
|
37
42
|
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["twMerge"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["twMerge","clsx"],"mappings":";;;;;;;;;;AAUO,SAAS,YAAuD,OAAA,EAAmB;AACtF,EAAA,MAAM,WAAmC,MAAA,CAAO,MAAA,CAAO,EAAC,EAAG,GAAG,OAAO,CAAA;AAQrE,EAAA,MAAM,OAAA,GAAU,CAAC,iBAAA,EAA2B,KAAA,KAAuB;AAC/D,IAAA,IAAI,CAAC,OAAO,OAAO,EAAA;AAGnB,IAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC3B,MAAA,MAAM,QAAA,GAAW,kBACZ,KAAA,CAAM,GAAG,EACT,GAAA,CAAI,CAAC,SAAU,IAAA,KAAS,MAAA,GAAS,OAAO,QAAA,CAAS,IAAI,KAAK,IAAK,CAAA,CAC/D,OAAO,OAAO,CAAA,CACd,KAAK,GAAG,CAAA;AAEb,MAAA,OAAO,MACF,KAAA,CAAM,UAAU,EAChB,MAAA,CAAO,OAAO,EACd,GAAA,CAAI,CAAC,QAAS,QAAA,GAAW,CAAA,EAAG,QAAQ,CAAA,CAAA,EAAI,GAAG,KAAK,GAAI,CAAA,CACpD,KAAK,GAAG,CAAA;AAAA,IACjB;AAGA,IAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACtB,MAAA,OAAO,KAAA,CACF,GAAA,CAAI,CAAC,CAAA,KAAM,OAAA,CAAQ,iBAAA,EAAmB,CAAC,CAAC,CAAA,CACxC,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,GAAA,EAAK,KAAK,CAAA,KAAM;AACnB,QAAA,IAAI,CAAC,OAAO,OAAO,EAAA;AAEnB,QAAA,MAAM,SAAS,GAAA,KAAQ,MAAA;AACvB,QAAA,MAAM,QAAA,GAAW,QAAA,CAAS,GAAG,CAAA,KAAM,MAAA;AAEnC,QAAA,IAAI,UAAU,QAAA,EAAU;AAEpB,UAAA,MAAM,aAAa,iBAAA,GAAoB,CAAA,EAAG,iBAAiB,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA,GAAK,GAAA;AACvE,UAAA,OAAO,OAAA,CAAQ,YAAY,KAAK,CAAA;AAAA,QACpC,CAAA,MAAO;AAEH,UAAA,OAAO,OAAA,CAAQ,mBAAmB,GAAG,CAAA;AAAA,QACzC;AAAA,MACJ,CAAC,CAAA,CACA,MAAA,CAAO,OAAO,CAAA,CACd,KAAK,GAAG,CAAA;AAAA,IACjB;AAEA,IAAA,OAAO,EAAA;AAAA,EACX,CAAA;AAUA,EAAA,OAAO,IAAI,MAAA,KAAyB;AAChC,IAAA,MAAM,SAAA,GAAY,OAAO,GAAA,CAAI,CAAC,UAAU,OAAA,CAAQ,EAAA,EAAI,KAAK,CAAC,CAAA;AAC1D,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, { type ClassValue } from 'clsx';\r\n\r\n/**\r\n * Creates a customized Tailwind class engine instance with prefix registry support.\r\n * * @param plugins - Objects mapping custom aliases (e.g., 'ui') to real Tailwind prefixes (e.g., 'prefix').\r\n * @returns A recursive 'cl' function that processes strings, arrays, and objects.\r\n * * @example\r\n * const tw = createCl({ md: 'md', ui: 'prefix' });\r\n */\r\nexport function createCl<TPlugins extends Record<string, string>[]>(...plugins: TPlugins) {\r\n const registry: Record<string, string> = Object.assign({}, ...plugins);\r\n\r\n /**\r\n * Recursively processes input values to apply prefixes and logic.\r\n * * @param accumulatedPrefix - The prefix path built during recursion (e.g., 'md:hover').\r\n * @param input - The value to process (string, array, or object).\r\n * @returns A string of prefixed and filtered Tailwind classes.\r\n */\r\n const process = (accumulatedPrefix: string, input: any): string => {\r\n if (!input) return '';\r\n\r\n // 1. Strings: Resolve real Tailwind prefixes and apply them\r\n if (typeof input === 'string') {\r\n const resolved = accumulatedPrefix\r\n .split(':')\r\n .map((part) => (part === 'base' ? null : registry[part] || null))\r\n .filter(Boolean)\r\n .join(':');\r\n\r\n return input\r\n .split(/[,\\s\\n]+/) // Split by commas, spaces, or newlines\r\n .filter(Boolean)\r\n .map((cls) => (resolved ? `${resolved}:${cls}` : cls))\r\n .join(' ');\r\n }\r\n\r\n // 2. Arrays: Multi-line support and recursive processing\r\n if (Array.isArray(input)) {\r\n return input\r\n .map((i) => process(accumulatedPrefix, i))\r\n .filter(Boolean)\r\n .join(' ');\r\n }\r\n\r\n // 3. Objects: Prefix navigation and Conditional Logic (clsx-style)\r\n if (typeof input === 'object') {\r\n return Object.entries(input)\r\n .map(([key, value]) => {\r\n if (!value) return '';\r\n\r\n const isBase = key === 'base';\r\n const isPrefix = registry[key] !== undefined;\r\n\r\n if (isBase || isPrefix) {\r\n // It's an organization node or a prefix: accumulate and dive deeper\r\n const nextPrefix = accumulatedPrefix ? `${accumulatedPrefix}:${key}` : key;\r\n return process(nextPrefix, value);\r\n } else {\r\n // Standard logic { 'class-name': boolean }: treat the key as the class content\r\n return process(accumulatedPrefix, key);\r\n }\r\n })\r\n .filter(Boolean)\r\n .join(' ');\r\n }\r\n\r\n return '';\r\n };\r\n\r\n /**\r\n * Main utility for generating Tailwind classes.\r\n * Supports strings, nested objects with prefixes, and arrays.\r\n * * @param inputs - A list of arguments following the clsx pattern.\r\n * @returns A processed, deduplicated string of classes via twMerge.\r\n * * @example\r\n * tw('btn-base', { md: 'p-4', hover: { 'opacity-50': isDim } });\r\n */\r\n return (...inputs: ClassValue[]) => {\r\n const processed = inputs.map((input) => process('', input));\r\n return twMerge(clsx(processed));\r\n };\r\n}\r\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { ClassValue } from 'clsx';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Creates a customized Tailwind class engine instance with prefix registry support.
|
|
5
|
+
* * @param plugins - Objects mapping custom aliases (e.g., 'ui') to real Tailwind prefixes (e.g., 'prefix').
|
|
6
|
+
* @returns A recursive 'cl' function that processes strings, arrays, and objects.
|
|
7
|
+
* * @example
|
|
8
|
+
* const tw = createCl({ md: 'md', ui: 'prefix' });
|
|
9
|
+
*/
|
|
3
10
|
declare function createCl<TPlugins extends Record<string, string>[]>(...plugins: TPlugins): (...inputs: ClassValue[]) => string;
|
|
4
11
|
|
|
5
12
|
export { createCl };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { ClassValue } from 'clsx';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Creates a customized Tailwind class engine instance with prefix registry support.
|
|
5
|
+
* * @param plugins - Objects mapping custom aliases (e.g., 'ui') to real Tailwind prefixes (e.g., 'prefix').
|
|
6
|
+
* @returns A recursive 'cl' function that processes strings, arrays, and objects.
|
|
7
|
+
* * @example
|
|
8
|
+
* const tw = createCl({ md: 'md', ui: 'prefix' });
|
|
9
|
+
*/
|
|
3
10
|
declare function createCl<TPlugins extends Record<string, string>[]>(...plugins: TPlugins): (...inputs: ClassValue[]) => string;
|
|
4
11
|
|
|
5
12
|
export { createCl };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { twMerge } from 'tailwind-merge';
|
|
2
|
+
import clsx from 'clsx';
|
|
2
3
|
|
|
3
4
|
// src/index.ts
|
|
4
5
|
function createCl(...plugins) {
|
|
@@ -6,30 +7,30 @@ function createCl(...plugins) {
|
|
|
6
7
|
const process = (accumulatedPrefix, input) => {
|
|
7
8
|
if (!input) return "";
|
|
8
9
|
if (typeof input === "string") {
|
|
9
|
-
|
|
10
|
+
const resolved = accumulatedPrefix.split(":").map((part) => part === "base" ? null : registry[part] || null).filter(Boolean).join(":");
|
|
11
|
+
return input.split(/[,\s\n]+/).filter(Boolean).map((cls) => resolved ? `${resolved}:${cls}` : cls).join(" ");
|
|
10
12
|
}
|
|
11
13
|
if (Array.isArray(input)) {
|
|
12
14
|
return input.map((i) => process(accumulatedPrefix, i)).filter(Boolean).join(" ");
|
|
13
15
|
}
|
|
14
16
|
if (typeof input === "object") {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
20
|
-
const nextPrefix = accumulatedPrefix ? `${accumulatedPrefix}:${
|
|
21
|
-
|
|
17
|
+
return Object.entries(input).map(([key, value]) => {
|
|
18
|
+
if (!value) return "";
|
|
19
|
+
const isBase = key === "base";
|
|
20
|
+
const isPrefix = registry[key] !== void 0;
|
|
21
|
+
if (isBase || isPrefix) {
|
|
22
|
+
const nextPrefix = accumulatedPrefix ? `${accumulatedPrefix}:${key}` : key;
|
|
23
|
+
return process(nextPrefix, value);
|
|
22
24
|
} else {
|
|
23
|
-
|
|
25
|
+
return process(accumulatedPrefix, key);
|
|
24
26
|
}
|
|
25
|
-
}
|
|
26
|
-
return results.join(" ");
|
|
27
|
+
}).filter(Boolean).join(" ");
|
|
27
28
|
}
|
|
28
29
|
return "";
|
|
29
30
|
};
|
|
30
31
|
return (...inputs) => {
|
|
31
32
|
const processed = inputs.map((input) => process("", input));
|
|
32
|
-
return twMerge(processed
|
|
33
|
+
return twMerge(clsx(processed));
|
|
33
34
|
};
|
|
34
35
|
}
|
|
35
36
|
|
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":";;;;AAUO,SAAS,YAAuD,OAAA,EAAmB;AACtF,EAAA,MAAM,WAAmC,MAAA,CAAO,MAAA,CAAO,EAAC,EAAG,GAAG,OAAO,CAAA;AAQrE,EAAA,MAAM,OAAA,GAAU,CAAC,iBAAA,EAA2B,KAAA,KAAuB;AAC/D,IAAA,IAAI,CAAC,OAAO,OAAO,EAAA;AAGnB,IAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC3B,MAAA,MAAM,QAAA,GAAW,kBACZ,KAAA,CAAM,GAAG,EACT,GAAA,CAAI,CAAC,SAAU,IAAA,KAAS,MAAA,GAAS,OAAO,QAAA,CAAS,IAAI,KAAK,IAAK,CAAA,CAC/D,OAAO,OAAO,CAAA,CACd,KAAK,GAAG,CAAA;AAEb,MAAA,OAAO,MACF,KAAA,CAAM,UAAU,EAChB,MAAA,CAAO,OAAO,EACd,GAAA,CAAI,CAAC,QAAS,QAAA,GAAW,CAAA,EAAG,QAAQ,CAAA,CAAA,EAAI,GAAG,KAAK,GAAI,CAAA,CACpD,KAAK,GAAG,CAAA;AAAA,IACjB;AAGA,IAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACtB,MAAA,OAAO,KAAA,CACF,GAAA,CAAI,CAAC,CAAA,KAAM,OAAA,CAAQ,iBAAA,EAAmB,CAAC,CAAC,CAAA,CACxC,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,GAAA,EAAK,KAAK,CAAA,KAAM;AACnB,QAAA,IAAI,CAAC,OAAO,OAAO,EAAA;AAEnB,QAAA,MAAM,SAAS,GAAA,KAAQ,MAAA;AACvB,QAAA,MAAM,QAAA,GAAW,QAAA,CAAS,GAAG,CAAA,KAAM,MAAA;AAEnC,QAAA,IAAI,UAAU,QAAA,EAAU;AAEpB,UAAA,MAAM,aAAa,iBAAA,GAAoB,CAAA,EAAG,iBAAiB,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA,GAAK,GAAA;AACvE,UAAA,OAAO,OAAA,CAAQ,YAAY,KAAK,CAAA;AAAA,QACpC,CAAA,MAAO;AAEH,UAAA,OAAO,OAAA,CAAQ,mBAAmB,GAAG,CAAA;AAAA,QACzC;AAAA,MACJ,CAAC,CAAA,CACA,MAAA,CAAO,OAAO,CAAA,CACd,KAAK,GAAG,CAAA;AAAA,IACjB;AAEA,IAAA,OAAO,EAAA;AAAA,EACX,CAAA;AAUA,EAAA,OAAO,IAAI,MAAA,KAAyB;AAChC,IAAA,MAAM,SAAA,GAAY,OAAO,GAAA,CAAI,CAAC,UAAU,OAAA,CAAQ,EAAA,EAAI,KAAK,CAAC,CAAA;AAC1D,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, { type ClassValue } from 'clsx';\r\n\r\n/**\r\n * Creates a customized Tailwind class engine instance with prefix registry support.\r\n * * @param plugins - Objects mapping custom aliases (e.g., 'ui') to real Tailwind prefixes (e.g., 'prefix').\r\n * @returns A recursive 'cl' function that processes strings, arrays, and objects.\r\n * * @example\r\n * const tw = createCl({ md: 'md', ui: 'prefix' });\r\n */\r\nexport function createCl<TPlugins extends Record<string, string>[]>(...plugins: TPlugins) {\r\n const registry: Record<string, string> = Object.assign({}, ...plugins);\r\n\r\n /**\r\n * Recursively processes input values to apply prefixes and logic.\r\n * * @param accumulatedPrefix - The prefix path built during recursion (e.g., 'md:hover').\r\n * @param input - The value to process (string, array, or object).\r\n * @returns A string of prefixed and filtered Tailwind classes.\r\n */\r\n const process = (accumulatedPrefix: string, input: any): string => {\r\n if (!input) return '';\r\n\r\n // 1. Strings: Resolve real Tailwind prefixes and apply them\r\n if (typeof input === 'string') {\r\n const resolved = accumulatedPrefix\r\n .split(':')\r\n .map((part) => (part === 'base' ? null : registry[part] || null))\r\n .filter(Boolean)\r\n .join(':');\r\n\r\n return input\r\n .split(/[,\\s\\n]+/) // Split by commas, spaces, or newlines\r\n .filter(Boolean)\r\n .map((cls) => (resolved ? `${resolved}:${cls}` : cls))\r\n .join(' ');\r\n }\r\n\r\n // 2. Arrays: Multi-line support and recursive processing\r\n if (Array.isArray(input)) {\r\n return input\r\n .map((i) => process(accumulatedPrefix, i))\r\n .filter(Boolean)\r\n .join(' ');\r\n }\r\n\r\n // 3. Objects: Prefix navigation and Conditional Logic (clsx-style)\r\n if (typeof input === 'object') {\r\n return Object.entries(input)\r\n .map(([key, value]) => {\r\n if (!value) return '';\r\n\r\n const isBase = key === 'base';\r\n const isPrefix = registry[key] !== undefined;\r\n\r\n if (isBase || isPrefix) {\r\n // It's an organization node or a prefix: accumulate and dive deeper\r\n const nextPrefix = accumulatedPrefix ? `${accumulatedPrefix}:${key}` : key;\r\n return process(nextPrefix, value);\r\n } else {\r\n // Standard logic { 'class-name': boolean }: treat the key as the class content\r\n return process(accumulatedPrefix, key);\r\n }\r\n })\r\n .filter(Boolean)\r\n .join(' ');\r\n }\r\n\r\n return '';\r\n };\r\n\r\n /**\r\n * Main utility for generating Tailwind classes.\r\n * Supports strings, nested objects with prefixes, and arrays.\r\n * * @param inputs - A list of arguments following the clsx pattern.\r\n * @returns A processed, deduplicated string of classes via twMerge.\r\n * * @example\r\n * tw('btn-base', { md: 'p-4', hover: { 'opacity-50': isDim } });\r\n */\r\n return (...inputs: ClassValue[]) => {\r\n const processed = inputs.map((input) => process('', input));\r\n return twMerge(clsx(processed));\r\n };\r\n}\r\n"]}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,22 +1,41 @@
|
|
|
1
1
|
import { twMerge } from 'tailwind-merge';
|
|
2
2
|
import clsx, { type ClassValue } from 'clsx';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Creates a customized Tailwind class engine instance with prefix registry support.
|
|
6
|
+
* * @param plugins - Objects mapping custom aliases (e.g., 'ui') to real Tailwind prefixes (e.g., 'prefix').
|
|
7
|
+
* @returns A recursive 'cl' function that processes strings, arrays, and objects.
|
|
8
|
+
* * @example
|
|
9
|
+
* const tw = createCl({ md: 'md', ui: 'prefix' });
|
|
10
|
+
*/
|
|
4
11
|
export function createCl<TPlugins extends Record<string, string>[]>(...plugins: TPlugins) {
|
|
5
12
|
const registry: Record<string, string> = Object.assign({}, ...plugins);
|
|
6
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Recursively processes input values to apply prefixes and logic.
|
|
16
|
+
* * @param accumulatedPrefix - The prefix path built during recursion (e.g., 'md:hover').
|
|
17
|
+
* @param input - The value to process (string, array, or object).
|
|
18
|
+
* @returns A string of prefixed and filtered Tailwind classes.
|
|
19
|
+
*/
|
|
7
20
|
const process = (accumulatedPrefix: string, input: any): string => {
|
|
8
21
|
if (!input) return '';
|
|
9
22
|
|
|
10
|
-
// 1. Strings:
|
|
23
|
+
// 1. Strings: Resolve real Tailwind prefixes and apply them
|
|
11
24
|
if (typeof input === 'string') {
|
|
25
|
+
const resolved = accumulatedPrefix
|
|
26
|
+
.split(':')
|
|
27
|
+
.map((part) => (part === 'base' ? null : registry[part] || null))
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
.join(':');
|
|
30
|
+
|
|
12
31
|
return input
|
|
13
|
-
.split(/[,\s\n]+/)
|
|
32
|
+
.split(/[,\s\n]+/) // Split by commas, spaces, or newlines
|
|
14
33
|
.filter(Boolean)
|
|
15
|
-
.map((cls) => (
|
|
34
|
+
.map((cls) => (resolved ? `${resolved}:${cls}` : cls))
|
|
16
35
|
.join(' ');
|
|
17
36
|
}
|
|
18
37
|
|
|
19
|
-
// 2. Arrays:
|
|
38
|
+
// 2. Arrays: Multi-line support and recursive processing
|
|
20
39
|
if (Array.isArray(input)) {
|
|
21
40
|
return input
|
|
22
41
|
.map((i) => process(accumulatedPrefix, i))
|
|
@@ -24,37 +43,41 @@ export function createCl<TPlugins extends Record<string, string>[]>(...plugins:
|
|
|
24
43
|
.join(' ');
|
|
25
44
|
}
|
|
26
45
|
|
|
27
|
-
// 3.
|
|
46
|
+
// 3. Objects: Prefix navigation and Conditional Logic (clsx-style)
|
|
28
47
|
if (typeof input === 'object') {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
return results.join(' ');
|
|
48
|
+
return Object.entries(input)
|
|
49
|
+
.map(([key, value]) => {
|
|
50
|
+
if (!value) return '';
|
|
51
|
+
|
|
52
|
+
const isBase = key === 'base';
|
|
53
|
+
const isPrefix = registry[key] !== undefined;
|
|
54
|
+
|
|
55
|
+
if (isBase || isPrefix) {
|
|
56
|
+
// It's an organization node or a prefix: accumulate and dive deeper
|
|
57
|
+
const nextPrefix = accumulatedPrefix ? `${accumulatedPrefix}:${key}` : key;
|
|
58
|
+
return process(nextPrefix, value);
|
|
59
|
+
} else {
|
|
60
|
+
// Standard logic { 'class-name': boolean }: treat the key as the class content
|
|
61
|
+
return process(accumulatedPrefix, key);
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.join(' ');
|
|
49
66
|
}
|
|
50
67
|
|
|
51
68
|
return '';
|
|
52
69
|
};
|
|
53
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Main utility for generating Tailwind classes.
|
|
73
|
+
* Supports strings, nested objects with prefixes, and arrays.
|
|
74
|
+
* * @param inputs - A list of arguments following the clsx pattern.
|
|
75
|
+
* @returns A processed, deduplicated string of classes via twMerge.
|
|
76
|
+
* * @example
|
|
77
|
+
* tw('btn-base', { md: 'p-4', hover: { 'opacity-50': isDim } });
|
|
78
|
+
*/
|
|
54
79
|
return (...inputs: ClassValue[]) => {
|
|
55
|
-
// Ejecutamos nuestro motor en cada input
|
|
56
80
|
const processed = inputs.map((input) => process('', input));
|
|
57
|
-
|
|
58
|
-
return twMerge(processed.filter(Boolean).join(' '));
|
|
81
|
+
return twMerge(clsx(processed));
|
|
59
82
|
};
|
|
60
83
|
}
|
package/src/test/cn.test.ts
CHANGED
|
@@ -70,4 +70,29 @@ describe('cl - Prefix Engine (Scope: Prefixes + clsx + twMerge)', () => {
|
|
|
70
70
|
});
|
|
71
71
|
expect(result).toBe('base-btn bg-blue-500 md:px-4 md:text-white');
|
|
72
72
|
});
|
|
73
|
+
|
|
74
|
+
it('Nivel 8: Lógica Condicional Anidada dentro de Prefijos', () => {
|
|
75
|
+
const isActive = true;
|
|
76
|
+
const isError = false;
|
|
77
|
+
// Forzamos el tipo para que TS permita comparar con 'light' en el test
|
|
78
|
+
const theme = 'dark' as 'light' | 'dark';
|
|
79
|
+
|
|
80
|
+
const result = cl({
|
|
81
|
+
md: {
|
|
82
|
+
// Objeto de lógica pura dentro de un prefijo
|
|
83
|
+
'bg-green-500': isActive,
|
|
84
|
+
'bg-red-500': isError,
|
|
85
|
+
// Combinación con otro prefijo anidado
|
|
86
|
+
dark: {
|
|
87
|
+
'text-white': theme === 'dark',
|
|
88
|
+
'text-black': theme === 'light' // Ahora TS ya no se queja
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(result).toContain('md:bg-green-500');
|
|
94
|
+
expect(result).toContain('md:dark:text-white');
|
|
95
|
+
expect(result).not.toContain('bg-red-500');
|
|
96
|
+
expect(result).not.toContain('text-black');
|
|
97
|
+
});
|
|
73
98
|
});
|