@mgcrea/react-native-tailwind 0.6.1 → 0.7.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.
Files changed (40) hide show
  1. package/README.md +404 -0
  2. package/dist/babel/config-loader.ts +1 -23
  3. package/dist/babel/index.cjs +227 -60
  4. package/dist/babel/index.d.ts +27 -2
  5. package/dist/babel/index.test.ts +268 -0
  6. package/dist/babel/index.ts +352 -44
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.js +1 -1
  9. package/dist/parser/__snapshots__/colors.test.js.snap +242 -90
  10. package/dist/parser/__snapshots__/transforms.test.js.snap +58 -0
  11. package/dist/parser/colors.js +1 -1
  12. package/dist/runtime.cjs +2 -0
  13. package/dist/runtime.cjs.map +7 -0
  14. package/dist/runtime.d.ts +139 -0
  15. package/dist/runtime.js +2 -0
  16. package/dist/runtime.js.map +7 -0
  17. package/dist/runtime.test.js +1 -0
  18. package/dist/stubs/tw.d.ts +60 -0
  19. package/dist/stubs/tw.js +1 -0
  20. package/dist/utils/flattenColors.d.ts +16 -0
  21. package/dist/utils/flattenColors.js +1 -0
  22. package/dist/utils/flattenColors.test.js +1 -0
  23. package/dist/utils/modifiers.d.ts +29 -0
  24. package/dist/utils/modifiers.js +1 -0
  25. package/dist/utils/modifiers.test.js +1 -0
  26. package/dist/utils/styleKey.test.js +1 -0
  27. package/package.json +15 -3
  28. package/src/babel/config-loader.ts +1 -23
  29. package/src/babel/index.test.ts +268 -0
  30. package/src/babel/index.ts +352 -44
  31. package/src/index.ts +5 -0
  32. package/src/parser/colors.ts +8 -22
  33. package/src/runtime.test.ts +325 -0
  34. package/src/runtime.ts +280 -0
  35. package/src/stubs/tw.ts +80 -0
  36. package/src/utils/flattenColors.test.ts +361 -0
  37. package/src/utils/flattenColors.ts +32 -0
  38. package/src/utils/modifiers.test.ts +286 -0
  39. package/src/utils/modifiers.ts +63 -0
  40. package/src/utils/styleKey.test.ts +168 -0
@@ -0,0 +1,361 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { flattenColors } from "./flattenColors";
3
+
4
+ describe("flattenColors", () => {
5
+ it("should handle flat color objects", () => {
6
+ const colors = {
7
+ red: "#ff0000",
8
+ blue: "#0000ff",
9
+ green: "#00ff00",
10
+ };
11
+
12
+ expect(flattenColors(colors)).toEqual({
13
+ red: "#ff0000",
14
+ blue: "#0000ff",
15
+ green: "#00ff00",
16
+ });
17
+ });
18
+
19
+ it("should flatten single-level nested objects", () => {
20
+ const colors = {
21
+ brand: {
22
+ primary: "#ff6b6b",
23
+ secondary: "#4ecdc4",
24
+ },
25
+ };
26
+
27
+ expect(flattenColors(colors)).toEqual({
28
+ "brand-primary": "#ff6b6b",
29
+ "brand-secondary": "#4ecdc4",
30
+ });
31
+ });
32
+
33
+ it("should flatten multi-level nested objects", () => {
34
+ const colors = {
35
+ brand: {
36
+ light: {
37
+ primary: "#ffcccc",
38
+ secondary: "#ccffff",
39
+ },
40
+ dark: {
41
+ primary: "#990000",
42
+ secondary: "#006666",
43
+ },
44
+ },
45
+ };
46
+
47
+ expect(flattenColors(colors)).toEqual({
48
+ "brand-light-primary": "#ffcccc",
49
+ "brand-light-secondary": "#ccffff",
50
+ "brand-dark-primary": "#990000",
51
+ "brand-dark-secondary": "#006666",
52
+ });
53
+ });
54
+
55
+ it("should handle mixed flat and nested objects", () => {
56
+ const colors = {
57
+ white: "#ffffff",
58
+ black: "#000000",
59
+ brand: {
60
+ primary: "#ff6b6b",
61
+ secondary: "#4ecdc4",
62
+ },
63
+ accent: "#ffe66d",
64
+ };
65
+
66
+ expect(flattenColors(colors)).toEqual({
67
+ white: "#ffffff",
68
+ black: "#000000",
69
+ "brand-primary": "#ff6b6b",
70
+ "brand-secondary": "#4ecdc4",
71
+ accent: "#ffe66d",
72
+ });
73
+ });
74
+
75
+ it("should handle Tailwind-style color scale objects", () => {
76
+ const colors = {
77
+ gray: {
78
+ "50": "#f9fafb",
79
+ "100": "#f3f4f6",
80
+ "200": "#e5e7eb",
81
+ "500": "#6b7280",
82
+ "900": "#111827",
83
+ },
84
+ };
85
+
86
+ expect(flattenColors(colors)).toEqual({
87
+ "gray-50": "#f9fafb",
88
+ "gray-100": "#f3f4f6",
89
+ "gray-200": "#e5e7eb",
90
+ "gray-500": "#6b7280",
91
+ "gray-900": "#111827",
92
+ });
93
+ });
94
+
95
+ it("should handle empty object", () => {
96
+ expect(flattenColors({})).toEqual({});
97
+ });
98
+
99
+ it("should handle single color", () => {
100
+ const colors = {
101
+ primary: "#ff0000",
102
+ };
103
+
104
+ expect(flattenColors(colors)).toEqual({
105
+ primary: "#ff0000",
106
+ });
107
+ });
108
+
109
+ it("should handle deeply nested objects (3+ levels)", () => {
110
+ const colors = {
111
+ theme: {
112
+ light: {
113
+ brand: {
114
+ primary: "#ff6b6b",
115
+ secondary: "#4ecdc4",
116
+ },
117
+ },
118
+ dark: {
119
+ brand: {
120
+ primary: "#990000",
121
+ secondary: "#006666",
122
+ },
123
+ },
124
+ },
125
+ };
126
+
127
+ expect(flattenColors(colors)).toEqual({
128
+ "theme-light-brand-primary": "#ff6b6b",
129
+ "theme-light-brand-secondary": "#4ecdc4",
130
+ "theme-dark-brand-primary": "#990000",
131
+ "theme-dark-brand-secondary": "#006666",
132
+ });
133
+ });
134
+
135
+ it("should handle numeric keys", () => {
136
+ const colors = {
137
+ blue: {
138
+ "100": "#dbeafe",
139
+ "500": "#3b82f6",
140
+ "900": "#1e3a8a",
141
+ },
142
+ };
143
+
144
+ expect(flattenColors(colors)).toEqual({
145
+ "blue-100": "#dbeafe",
146
+ "blue-500": "#3b82f6",
147
+ "blue-900": "#1e3a8a",
148
+ });
149
+ });
150
+
151
+ it("should handle keys with hyphens", () => {
152
+ const colors = {
153
+ "brand-primary": "#ff0000",
154
+ "brand-secondary": {
155
+ light: "#00ff00",
156
+ dark: "#006600",
157
+ },
158
+ };
159
+
160
+ expect(flattenColors(colors)).toEqual({
161
+ "brand-primary": "#ff0000",
162
+ "brand-secondary-light": "#00ff00",
163
+ "brand-secondary-dark": "#006600",
164
+ });
165
+ });
166
+
167
+ it("should handle uppercase and lowercase hex values", () => {
168
+ const colors = {
169
+ red: "#FF0000",
170
+ blue: "#0000ff",
171
+ green: "#00Ff00",
172
+ };
173
+
174
+ expect(flattenColors(colors)).toEqual({
175
+ red: "#FF0000",
176
+ blue: "#0000ff",
177
+ green: "#00Ff00",
178
+ });
179
+ });
180
+
181
+ it("should handle 3-digit hex values", () => {
182
+ const colors = {
183
+ red: "#f00",
184
+ blue: "#00f",
185
+ green: "#0f0",
186
+ };
187
+
188
+ expect(flattenColors(colors)).toEqual({
189
+ red: "#f00",
190
+ blue: "#00f",
191
+ green: "#0f0",
192
+ });
193
+ });
194
+
195
+ it("should handle 8-digit hex values (with alpha)", () => {
196
+ const colors = {
197
+ "red-50": "#ff000080",
198
+ "blue-50": "#0000ff80",
199
+ };
200
+
201
+ expect(flattenColors(colors)).toEqual({
202
+ "red-50": "#ff000080",
203
+ "blue-50": "#0000ff80",
204
+ });
205
+ });
206
+
207
+ it("should handle complex real-world Tailwind config", () => {
208
+ const colors = {
209
+ transparent: "transparent",
210
+ current: "currentColor",
211
+ white: "#ffffff",
212
+ black: "#000000",
213
+ gray: {
214
+ "50": "#f9fafb",
215
+ "100": "#f3f4f6",
216
+ "500": "#6b7280",
217
+ "900": "#111827",
218
+ },
219
+ brand: {
220
+ primary: "#ff6b6b",
221
+ secondary: "#4ecdc4",
222
+ accent: {
223
+ light: "#ffe66d",
224
+ dark: "#ffb900",
225
+ },
226
+ },
227
+ };
228
+
229
+ expect(flattenColors(colors)).toEqual({
230
+ transparent: "transparent",
231
+ current: "currentColor",
232
+ white: "#ffffff",
233
+ black: "#000000",
234
+ "gray-50": "#f9fafb",
235
+ "gray-100": "#f3f4f6",
236
+ "gray-500": "#6b7280",
237
+ "gray-900": "#111827",
238
+ "brand-primary": "#ff6b6b",
239
+ "brand-secondary": "#4ecdc4",
240
+ "brand-accent-light": "#ffe66d",
241
+ "brand-accent-dark": "#ffb900",
242
+ });
243
+ });
244
+
245
+ it("should not mutate input object", () => {
246
+ const colors = {
247
+ brand: {
248
+ primary: "#ff6b6b",
249
+ secondary: "#4ecdc4",
250
+ },
251
+ };
252
+
253
+ const original = JSON.parse(JSON.stringify(colors));
254
+ flattenColors(colors);
255
+
256
+ expect(colors).toEqual(original);
257
+ });
258
+
259
+ it("should handle undefined values gracefully", () => {
260
+ const colors = {
261
+ red: "#ff0000",
262
+ blue: undefined as unknown as string, // Testing runtime edge case
263
+ green: "#00ff00",
264
+ };
265
+
266
+ // undefined values are skipped (not objects or strings)
267
+ expect(flattenColors(colors)).toEqual({
268
+ red: "#ff0000",
269
+ green: "#00ff00",
270
+ });
271
+ });
272
+
273
+ it("should handle special color keywords", () => {
274
+ const colors = {
275
+ transparent: "transparent",
276
+ current: "currentColor",
277
+ inherit: "inherit",
278
+ };
279
+
280
+ expect(flattenColors(colors)).toEqual({
281
+ transparent: "transparent",
282
+ current: "currentColor",
283
+ inherit: "inherit",
284
+ });
285
+ });
286
+
287
+ it("should handle RGB/RGBA color values", () => {
288
+ const colors = {
289
+ primary: "rgb(255, 0, 0)",
290
+ secondary: "rgba(0, 255, 0, 0.5)",
291
+ };
292
+
293
+ expect(flattenColors(colors)).toEqual({
294
+ primary: "rgb(255, 0, 0)",
295
+ secondary: "rgba(0, 255, 0, 0.5)",
296
+ });
297
+ });
298
+
299
+ it("should handle very deeply nested structures (stress test)", () => {
300
+ const colors = {
301
+ level1: {
302
+ level2: {
303
+ level3: {
304
+ level4: {
305
+ level5: "#ff0000",
306
+ },
307
+ },
308
+ },
309
+ },
310
+ };
311
+
312
+ expect(flattenColors(colors)).toEqual({
313
+ "level1-level2-level3-level4-level5": "#ff0000",
314
+ });
315
+ });
316
+
317
+ it("should handle camelCase keys", () => {
318
+ const colors = {
319
+ brandPrimary: "#ff0000",
320
+ accentColor: {
321
+ lightShade: "#ffcccc",
322
+ darkShade: "#cc0000",
323
+ },
324
+ };
325
+
326
+ expect(flattenColors(colors)).toEqual({
327
+ brandPrimary: "#ff0000",
328
+ "accentColor-lightShade": "#ffcccc",
329
+ "accentColor-darkShade": "#cc0000",
330
+ });
331
+ });
332
+
333
+ it("should produce consistent output", () => {
334
+ const colors = {
335
+ brand: {
336
+ primary: "#ff6b6b",
337
+ secondary: "#4ecdc4",
338
+ },
339
+ };
340
+
341
+ const result1 = flattenColors(colors);
342
+ const result2 = flattenColors(colors);
343
+ const result3 = flattenColors(colors);
344
+
345
+ expect(result1).toEqual(result2);
346
+ expect(result2).toEqual(result3);
347
+ });
348
+
349
+ it("should maintain key order (insertion order)", () => {
350
+ const colors = {
351
+ z: "#000001",
352
+ a: "#000002",
353
+ m: "#000003",
354
+ };
355
+
356
+ const flattened = flattenColors(colors);
357
+ const keys = Object.keys(flattened);
358
+
359
+ expect(keys).toEqual(["z", "a", "m"]);
360
+ });
361
+ });
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Type representing a nested color structure that can be arbitrarily deep
3
+ */
4
+ type NestedColors = {
5
+ [key: string]: string | NestedColors;
6
+ };
7
+
8
+ /**
9
+ * Flatten nested color objects into flat key-value map
10
+ * Example: { brand: { light: '#fff', dark: '#000' } } => { 'brand-light': '#fff', 'brand-dark': '#000' }
11
+ *
12
+ * @param colors - Nested color object where values can be strings or objects
13
+ * @param prefix - Optional prefix for nested keys (used for recursion)
14
+ * @returns Flattened color map with dash-separated keys
15
+ */
16
+ export function flattenColors(colors: NestedColors, prefix = ""): Record<string, string> {
17
+ const result: Record<string, string> = {};
18
+
19
+ for (const [key, value] of Object.entries(colors)) {
20
+ const newKey = prefix ? `${prefix}-${key}` : key;
21
+
22
+ if (typeof value === "string") {
23
+ result[newKey] = value;
24
+ } else if (typeof value === "object" && value !== null) {
25
+ // Recursively flatten nested objects
26
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
27
+ Object.assign(result, flattenColors(value as NestedColors, newKey));
28
+ }
29
+ }
30
+
31
+ return result;
32
+ }
@@ -0,0 +1,286 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { SUPPORTED_MODIFIERS, hasModifiers, splitModifierClasses } from "./modifiers";
3
+
4
+ describe("SUPPORTED_MODIFIERS", () => {
5
+ it("should export list of supported modifiers", () => {
6
+ expect(SUPPORTED_MODIFIERS).toEqual(["active", "focus", "disabled"]);
7
+ });
8
+
9
+ it("should be readonly", () => {
10
+ // TypeScript enforces this at compile time
11
+ expect(Array.isArray(SUPPORTED_MODIFIERS)).toBe(true);
12
+ });
13
+ });
14
+
15
+ describe("hasModifiers", () => {
16
+ it("should return true for active modifier", () => {
17
+ expect(hasModifiers("active:bg-blue-500")).toBe(true);
18
+ expect(hasModifiers("m-4 active:bg-blue-500")).toBe(true);
19
+ expect(hasModifiers("active:bg-blue-500 active:text-white")).toBe(true);
20
+ });
21
+
22
+ it("should return true for focus modifier", () => {
23
+ expect(hasModifiers("focus:border-blue-500")).toBe(true);
24
+ expect(hasModifiers("p-2 focus:border-blue-500")).toBe(true);
25
+ expect(hasModifiers("focus:bg-blue-500 focus:text-white")).toBe(true);
26
+ });
27
+
28
+ it("should return true for disabled modifier", () => {
29
+ expect(hasModifiers("disabled:opacity-50")).toBe(true);
30
+ expect(hasModifiers("bg-blue-500 disabled:opacity-50")).toBe(true);
31
+ expect(hasModifiers("disabled:bg-gray-300 disabled:text-gray-500")).toBe(true);
32
+ });
33
+
34
+ it("should return true for multiple different modifiers", () => {
35
+ expect(hasModifiers("active:bg-blue-700 focus:border-blue-500")).toBe(true);
36
+ expect(hasModifiers("bg-blue-500 active:bg-blue-700 disabled:opacity-50")).toBe(true);
37
+ expect(hasModifiers("focus:border-blue-500 disabled:bg-gray-300")).toBe(true);
38
+ });
39
+
40
+ it("should return false for no modifiers", () => {
41
+ expect(hasModifiers("bg-blue-500")).toBe(false);
42
+ expect(hasModifiers("m-4 p-2 text-white")).toBe(false);
43
+ expect(hasModifiers("flex items-center justify-center")).toBe(false);
44
+ });
45
+
46
+ it("should return false for empty string", () => {
47
+ expect(hasModifiers("")).toBe(false);
48
+ });
49
+
50
+ it("should return false for partial matches", () => {
51
+ expect(hasModifiers("active")).toBe(false);
52
+ expect(hasModifiers("focus")).toBe(false);
53
+ expect(hasModifiers("disabled")).toBe(false);
54
+ expect(hasModifiers("active-bg-blue-500")).toBe(false);
55
+ expect(hasModifiers("focusborder-blue-500")).toBe(false);
56
+ });
57
+
58
+ it("should detect modifiers anywhere in the string", () => {
59
+ expect(hasModifiers("bg-blue-500 active:bg-blue-700 text-white")).toBe(true);
60
+ expect(hasModifiers("flex items-center focus:border-blue-500 p-4")).toBe(true);
61
+ });
62
+
63
+ it("should handle whitespace variations", () => {
64
+ expect(hasModifiers(" active:bg-blue-500 ")).toBe(true);
65
+ expect(hasModifiers("\tactive:bg-blue-500\n")).toBe(true);
66
+ expect(hasModifiers("m-4 active:bg-blue-500 p-2")).toBe(true);
67
+ });
68
+
69
+ it("should be case-sensitive", () => {
70
+ expect(hasModifiers("Active:bg-blue-500")).toBe(false);
71
+ expect(hasModifiers("ACTIVE:bg-blue-500")).toBe(false);
72
+ expect(hasModifiers("Focus:border-blue-500")).toBe(false);
73
+ });
74
+ });
75
+
76
+ describe("splitModifierClasses", () => {
77
+ it("should split base classes without modifiers", () => {
78
+ const result = splitModifierClasses("m-4 p-2 bg-blue-500");
79
+
80
+ expect(result.base).toEqual(["m-4", "p-2", "bg-blue-500"]);
81
+ expect(result.modifiers.size).toBe(0);
82
+ });
83
+
84
+ it("should split active modifier classes", () => {
85
+ const result = splitModifierClasses("bg-blue-500 active:bg-blue-700");
86
+
87
+ expect(result.base).toEqual(["bg-blue-500"]);
88
+ expect(result.modifiers.get("active")).toEqual(["bg-blue-700"]);
89
+ });
90
+
91
+ it("should split focus modifier classes", () => {
92
+ const result = splitModifierClasses("border-gray-300 focus:border-blue-500");
93
+
94
+ expect(result.base).toEqual(["border-gray-300"]);
95
+ expect(result.modifiers.get("focus")).toEqual(["border-blue-500"]);
96
+ });
97
+
98
+ it("should split disabled modifier classes", () => {
99
+ const result = splitModifierClasses("bg-blue-500 disabled:bg-gray-300");
100
+
101
+ expect(result.base).toEqual(["bg-blue-500"]);
102
+ expect(result.modifiers.get("disabled")).toEqual(["bg-gray-300"]);
103
+ });
104
+
105
+ it("should split multiple classes with same modifier", () => {
106
+ const result = splitModifierClasses(
107
+ "bg-blue-500 active:bg-blue-700 active:text-white active:border-blue-900",
108
+ );
109
+
110
+ expect(result.base).toEqual(["bg-blue-500"]);
111
+ expect(result.modifiers.get("active")).toEqual(["bg-blue-700", "text-white", "border-blue-900"]);
112
+ });
113
+
114
+ it("should split multiple different modifiers", () => {
115
+ const result = splitModifierClasses(
116
+ "bg-blue-500 active:bg-blue-700 focus:border-blue-500 disabled:opacity-50",
117
+ );
118
+
119
+ expect(result.base).toEqual(["bg-blue-500"]);
120
+ expect(result.modifiers.get("active")).toEqual(["bg-blue-700"]);
121
+ expect(result.modifiers.get("focus")).toEqual(["border-blue-500"]);
122
+ expect(result.modifiers.get("disabled")).toEqual(["opacity-50"]);
123
+ });
124
+
125
+ it("should handle complex combination of base and modifier classes", () => {
126
+ const result = splitModifierClasses(
127
+ "m-4 p-2 bg-blue-500 text-white rounded-lg active:bg-blue-700 active:text-gray-100 focus:border-blue-500 disabled:opacity-50 disabled:bg-gray-300",
128
+ );
129
+
130
+ expect(result.base).toEqual(["m-4", "p-2", "bg-blue-500", "text-white", "rounded-lg"]);
131
+ expect(result.modifiers.get("active")).toEqual(["bg-blue-700", "text-gray-100"]);
132
+ expect(result.modifiers.get("focus")).toEqual(["border-blue-500"]);
133
+ expect(result.modifiers.get("disabled")).toEqual(["opacity-50", "bg-gray-300"]);
134
+ });
135
+
136
+ it("should handle empty string", () => {
137
+ const result = splitModifierClasses("");
138
+
139
+ expect(result.base).toEqual([]);
140
+ expect(result.modifiers.size).toBe(0);
141
+ });
142
+
143
+ it("should handle whitespace-only string", () => {
144
+ const result = splitModifierClasses(" ");
145
+
146
+ expect(result.base).toEqual([]);
147
+ expect(result.modifiers.size).toBe(0);
148
+ });
149
+
150
+ it("should handle only modifier classes", () => {
151
+ const result = splitModifierClasses("active:bg-blue-700 focus:border-blue-500");
152
+
153
+ expect(result.base).toEqual([]);
154
+ expect(result.modifiers.get("active")).toEqual(["bg-blue-700"]);
155
+ expect(result.modifiers.get("focus")).toEqual(["border-blue-500"]);
156
+ });
157
+
158
+ it("should filter out empty strings from split", () => {
159
+ const result = splitModifierClasses("m-4 p-2 bg-blue-500");
160
+
161
+ expect(result.base).toEqual(["m-4", "p-2", "bg-blue-500"]);
162
+ expect(result.modifiers.size).toBe(0);
163
+ });
164
+
165
+ it("should handle leading and trailing whitespace", () => {
166
+ const result = splitModifierClasses(" m-4 active:bg-blue-700 ");
167
+
168
+ expect(result.base).toEqual(["m-4"]);
169
+ expect(result.modifiers.get("active")).toEqual(["bg-blue-700"]);
170
+ });
171
+
172
+ it("should handle arbitrary values with modifiers", () => {
173
+ const result = splitModifierClasses("bg-[#ff0000] active:bg-[#00ff00] text-[14px]");
174
+
175
+ expect(result.base).toEqual(["bg-[#ff0000]", "text-[14px]"]);
176
+ expect(result.modifiers.get("active")).toEqual(["bg-[#00ff00]"]);
177
+ });
178
+
179
+ it("should handle opacity modifiers with state modifiers", () => {
180
+ const result = splitModifierClasses("bg-black/50 active:bg-black/80 text-white/90");
181
+
182
+ expect(result.base).toEqual(["bg-black/50", "text-white/90"]);
183
+ expect(result.modifiers.get("active")).toEqual(["bg-black/80"]);
184
+ });
185
+
186
+ it("should preserve order of base classes", () => {
187
+ const result = splitModifierClasses("z-10 m-4 a-1 p-2");
188
+
189
+ expect(result.base).toEqual(["z-10", "m-4", "a-1", "p-2"]);
190
+ });
191
+
192
+ it("should preserve order of modifier classes", () => {
193
+ const result = splitModifierClasses("active:z-10 active:m-4 active:a-1 active:p-2");
194
+
195
+ expect(result.modifiers.get("active")).toEqual(["z-10", "m-4", "a-1", "p-2"]);
196
+ });
197
+
198
+ it("should not match modifiers without colon", () => {
199
+ const result = splitModifierClasses("active bg-blue-500 focus border-blue-500");
200
+
201
+ expect(result.base).toEqual(["active", "bg-blue-500", "focus", "border-blue-500"]);
202
+ expect(result.modifiers.size).toBe(0);
203
+ });
204
+
205
+ it("should not match partial modifier names", () => {
206
+ const result = splitModifierClasses("reactive:bg-blue-500 prefocus:border-blue-500");
207
+
208
+ expect(result.base).toEqual(["reactive:bg-blue-500", "prefocus:border-blue-500"]);
209
+ expect(result.modifiers.size).toBe(0);
210
+ });
211
+
212
+ it("should handle modifiers at different positions", () => {
213
+ const result = splitModifierClasses(
214
+ "active:bg-blue-700 m-4 focus:border-blue-500 p-2 disabled:opacity-50",
215
+ );
216
+
217
+ expect(result.base).toEqual(["m-4", "p-2"]);
218
+ expect(result.modifiers.get("active")).toEqual(["bg-blue-700"]);
219
+ expect(result.modifiers.get("focus")).toEqual(["border-blue-500"]);
220
+ expect(result.modifiers.get("disabled")).toEqual(["opacity-50"]);
221
+ });
222
+
223
+ it("should return empty Map when no modifiers present", () => {
224
+ const result = splitModifierClasses("m-4 p-2 bg-blue-500");
225
+
226
+ expect(result.modifiers).toBeInstanceOf(Map);
227
+ expect(result.modifiers.size).toBe(0);
228
+ expect(result.modifiers.get("active")).toBeUndefined();
229
+ expect(result.modifiers.get("focus")).toBeUndefined();
230
+ expect(result.modifiers.get("disabled")).toBeUndefined();
231
+ });
232
+
233
+ it("should return Map with only present modifiers", () => {
234
+ const result = splitModifierClasses("bg-blue-500 active:bg-blue-700");
235
+
236
+ expect(result.modifiers.has("active")).toBe(true);
237
+ expect(result.modifiers.has("focus")).toBe(false);
238
+ expect(result.modifiers.has("disabled")).toBe(false);
239
+ });
240
+
241
+ it("should handle real-world button example", () => {
242
+ const result = splitModifierClasses(
243
+ "px-4 py-2 bg-blue-500 text-white rounded-lg active:bg-blue-700 disabled:bg-gray-300 disabled:text-gray-500",
244
+ );
245
+
246
+ expect(result.base).toEqual(["px-4", "py-2", "bg-blue-500", "text-white", "rounded-lg"]);
247
+ expect(result.modifiers.get("active")).toEqual(["bg-blue-700"]);
248
+ expect(result.modifiers.get("disabled")).toEqual(["bg-gray-300", "text-gray-500"]);
249
+ expect(result.modifiers.get("focus")).toBeUndefined();
250
+ });
251
+
252
+ it("should handle real-world input example", () => {
253
+ const result = splitModifierClasses(
254
+ "border border-gray-300 rounded p-2 focus:border-blue-500 focus:outline-none disabled:bg-gray-100 disabled:text-gray-400",
255
+ );
256
+
257
+ expect(result.base).toEqual(["border", "border-gray-300", "rounded", "p-2"]);
258
+ expect(result.modifiers.get("focus")).toEqual(["border-blue-500", "outline-none"]);
259
+ expect(result.modifiers.get("disabled")).toEqual(["bg-gray-100", "text-gray-400"]);
260
+ expect(result.modifiers.get("active")).toBeUndefined();
261
+ });
262
+
263
+ it("should be consistent across multiple calls", () => {
264
+ const className = "m-4 active:bg-blue-700 focus:border-blue-500";
265
+ const result1 = splitModifierClasses(className);
266
+ const result2 = splitModifierClasses(className);
267
+
268
+ expect(result1.base).toEqual(result2.base);
269
+ expect(result1.modifiers.get("active")).toEqual(result2.modifiers.get("active"));
270
+ expect(result1.modifiers.get("focus")).toEqual(result2.modifiers.get("focus"));
271
+ });
272
+
273
+ it("should handle negative values with modifiers", () => {
274
+ const result = splitModifierClasses("-m-4 active:-m-8 -translate-x-2");
275
+
276
+ expect(result.base).toEqual(["-m-4", "-translate-x-2"]);
277
+ expect(result.modifiers.get("active")).toEqual(["-m-8"]);
278
+ });
279
+
280
+ it("should handle transform classes with modifiers", () => {
281
+ const result = splitModifierClasses("scale-100 active:scale-110 rotate-0 active:rotate-45");
282
+
283
+ expect(result.base).toEqual(["scale-100", "rotate-0"]);
284
+ expect(result.modifiers.get("active")).toEqual(["scale-110", "rotate-45"]);
285
+ });
286
+ });