@mgcrea/react-native-tailwind 0.3.0 → 0.5.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 +459 -39
- package/dist/babel/index.cjs +810 -279
- package/dist/babel/index.d.ts +2 -1
- package/dist/babel/index.ts +328 -22
- package/dist/components/Pressable.d.ts +32 -0
- package/dist/components/Pressable.js +1 -0
- package/dist/components/TextInput.d.ts +56 -0
- package/dist/components/TextInput.js +1 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +1 -1
- package/dist/parser/aspectRatio.d.ts +16 -0
- package/dist/parser/aspectRatio.js +1 -0
- package/dist/parser/aspectRatio.test.d.ts +1 -0
- package/dist/parser/aspectRatio.test.js +1 -0
- package/dist/parser/borders.js +1 -1
- package/dist/parser/borders.test.d.ts +1 -0
- package/dist/parser/borders.test.js +1 -0
- package/dist/parser/colors.d.ts +1 -0
- package/dist/parser/colors.js +1 -1
- package/dist/parser/colors.test.d.ts +1 -0
- package/dist/parser/colors.test.js +1 -0
- package/dist/parser/index.d.ts +4 -0
- package/dist/parser/index.js +1 -1
- package/dist/parser/layout.d.ts +2 -0
- package/dist/parser/layout.js +1 -1
- package/dist/parser/layout.test.d.ts +1 -0
- package/dist/parser/layout.test.js +1 -0
- package/dist/parser/modifiers.d.ts +47 -0
- package/dist/parser/modifiers.js +1 -0
- package/dist/parser/modifiers.test.d.ts +1 -0
- package/dist/parser/modifiers.test.js +1 -0
- package/dist/parser/shadows.d.ts +26 -0
- package/dist/parser/shadows.js +1 -0
- package/dist/parser/shadows.test.d.ts +1 -0
- package/dist/parser/shadows.test.js +1 -0
- package/dist/parser/sizing.test.d.ts +1 -0
- package/dist/parser/sizing.test.js +1 -0
- package/dist/parser/spacing.d.ts +1 -1
- package/dist/parser/spacing.js +1 -1
- package/dist/parser/spacing.test.d.ts +1 -0
- package/dist/parser/spacing.test.js +1 -0
- package/dist/parser/typography.d.ts +2 -1
- package/dist/parser/typography.js +1 -1
- package/dist/parser/typography.test.d.ts +1 -0
- package/dist/parser/typography.test.js +1 -0
- package/dist/types.d.ts +5 -2
- package/package.json +7 -6
- package/src/babel/index.ts +328 -22
- package/src/components/Pressable.tsx +46 -0
- package/src/components/TextInput.tsx +90 -0
- package/src/index.ts +20 -2
- package/src/parser/aspectRatio.test.ts +191 -0
- package/src/parser/aspectRatio.ts +73 -0
- package/src/parser/borders.test.ts +329 -0
- package/src/parser/borders.ts +187 -108
- package/src/parser/colors.test.ts +335 -0
- package/src/parser/colors.ts +117 -6
- package/src/parser/index.ts +13 -2
- package/src/parser/layout.test.ts +459 -0
- package/src/parser/layout.ts +128 -0
- package/src/parser/modifiers.test.ts +375 -0
- package/src/parser/modifiers.ts +104 -0
- package/src/parser/shadows.test.ts +201 -0
- package/src/parser/shadows.ts +133 -0
- package/src/parser/sizing.test.ts +256 -0
- package/src/parser/spacing.test.ts +226 -0
- package/src/parser/spacing.ts +93 -138
- package/src/parser/typography.test.ts +221 -0
- package/src/parser/typography.ts +143 -112
- package/src/types.ts +2 -2
- package/dist/react-native.d.js +0 -1
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { ModifierType, ParsedModifier } from "./modifiers";
|
|
3
|
+
import { hasModifier, parseModifier, splitModifierClasses } from "./modifiers";
|
|
4
|
+
|
|
5
|
+
describe("parseModifier - basic functionality", () => {
|
|
6
|
+
it("should parse active modifier", () => {
|
|
7
|
+
const result = parseModifier("active:bg-blue-500");
|
|
8
|
+
expect(result).toEqual({
|
|
9
|
+
modifier: "active",
|
|
10
|
+
baseClass: "bg-blue-500",
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should parse hover modifier", () => {
|
|
15
|
+
const result = parseModifier("hover:text-red-500");
|
|
16
|
+
expect(result).toEqual({
|
|
17
|
+
modifier: "hover",
|
|
18
|
+
baseClass: "text-red-500",
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should parse focus modifier", () => {
|
|
23
|
+
const result = parseModifier("focus:border-green-500");
|
|
24
|
+
expect(result).toEqual({
|
|
25
|
+
modifier: "focus",
|
|
26
|
+
baseClass: "border-green-500",
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should parse disabled modifier", () => {
|
|
31
|
+
const result = parseModifier("disabled:bg-gray-400");
|
|
32
|
+
expect(result).toEqual({
|
|
33
|
+
modifier: "disabled",
|
|
34
|
+
baseClass: "bg-gray-400",
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should return null for class without modifier", () => {
|
|
39
|
+
expect(parseModifier("bg-blue-500")).toBeNull();
|
|
40
|
+
expect(parseModifier("text-red-500")).toBeNull();
|
|
41
|
+
expect(parseModifier("p-4")).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("parseModifier - various base classes", () => {
|
|
46
|
+
it("should parse modifiers with spacing classes", () => {
|
|
47
|
+
expect(parseModifier("active:m-4")).toEqual({
|
|
48
|
+
modifier: "active",
|
|
49
|
+
baseClass: "m-4",
|
|
50
|
+
});
|
|
51
|
+
expect(parseModifier("hover:p-8")).toEqual({
|
|
52
|
+
modifier: "hover",
|
|
53
|
+
baseClass: "p-8",
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should parse modifiers with layout classes", () => {
|
|
58
|
+
expect(parseModifier("active:flex")).toEqual({
|
|
59
|
+
modifier: "active",
|
|
60
|
+
baseClass: "flex",
|
|
61
|
+
});
|
|
62
|
+
expect(parseModifier("focus:absolute")).toEqual({
|
|
63
|
+
modifier: "focus",
|
|
64
|
+
baseClass: "absolute",
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should parse modifiers with typography classes", () => {
|
|
69
|
+
expect(parseModifier("hover:text-lg")).toEqual({
|
|
70
|
+
modifier: "hover",
|
|
71
|
+
baseClass: "text-lg",
|
|
72
|
+
});
|
|
73
|
+
expect(parseModifier("active:font-bold")).toEqual({
|
|
74
|
+
modifier: "active",
|
|
75
|
+
baseClass: "font-bold",
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should parse modifiers with arbitrary values", () => {
|
|
80
|
+
expect(parseModifier("active:bg-[#ff0000]")).toEqual({
|
|
81
|
+
modifier: "active",
|
|
82
|
+
baseClass: "bg-[#ff0000]",
|
|
83
|
+
});
|
|
84
|
+
expect(parseModifier("hover:w-[100px]")).toEqual({
|
|
85
|
+
modifier: "hover",
|
|
86
|
+
baseClass: "w-[100px]",
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should parse modifiers with complex class names", () => {
|
|
91
|
+
expect(parseModifier("active:inset-x-4")).toEqual({
|
|
92
|
+
modifier: "active",
|
|
93
|
+
baseClass: "inset-x-4",
|
|
94
|
+
});
|
|
95
|
+
expect(parseModifier("focus:border-blue-500")).toEqual({
|
|
96
|
+
modifier: "focus",
|
|
97
|
+
baseClass: "border-blue-500",
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("parseModifier - edge cases", () => {
|
|
103
|
+
it("should return null for unsupported modifiers", () => {
|
|
104
|
+
expect(parseModifier("selected:bg-blue-500")).toBeNull();
|
|
105
|
+
expect(parseModifier("pressed:bg-red-500")).toBeNull();
|
|
106
|
+
expect(parseModifier("custom:bg-green-500")).toBeNull();
|
|
107
|
+
expect(parseModifier("unknown:bg-gray-500")).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should return null for nested modifiers", () => {
|
|
111
|
+
expect(parseModifier("active:hover:bg-blue-500")).toBeNull();
|
|
112
|
+
expect(parseModifier("hover:focus:text-red-500")).toBeNull();
|
|
113
|
+
expect(parseModifier("focus:active:p-4")).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should return null for empty base class", () => {
|
|
117
|
+
expect(parseModifier("active:")).toBeNull();
|
|
118
|
+
expect(parseModifier("hover:")).toBeNull();
|
|
119
|
+
expect(parseModifier("focus:")).toBeNull();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should return null for empty modifier", () => {
|
|
123
|
+
expect(parseModifier(":bg-blue-500")).toBeNull();
|
|
124
|
+
expect(parseModifier(":")).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should return null for class with only colon", () => {
|
|
128
|
+
expect(parseModifier(":")).toBeNull();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should return null for class with multiple colons but no modifier", () => {
|
|
132
|
+
expect(parseModifier("bg:blue:500")).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should handle base classes that look like they have colons", () => {
|
|
136
|
+
// Base class contains colon - should be rejected as nested modifier
|
|
137
|
+
expect(parseModifier("active:some:thing")).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should return null for empty string", () => {
|
|
141
|
+
expect(parseModifier("")).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("parseModifier - case sensitivity", () => {
|
|
146
|
+
it("should be case-sensitive for modifiers", () => {
|
|
147
|
+
expect(parseModifier("Active:bg-blue-500")).toBeNull();
|
|
148
|
+
expect(parseModifier("ACTIVE:bg-blue-500")).toBeNull();
|
|
149
|
+
expect(parseModifier("Hover:text-red-500")).toBeNull();
|
|
150
|
+
expect(parseModifier("FOCUS:border-green-500")).toBeNull();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should preserve case in base class", () => {
|
|
154
|
+
const result = parseModifier("active:bg-Blue-500");
|
|
155
|
+
expect(result).toEqual({
|
|
156
|
+
modifier: "active",
|
|
157
|
+
baseClass: "bg-Blue-500",
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("hasModifier", () => {
|
|
163
|
+
it("should return true for classes with modifiers", () => {
|
|
164
|
+
expect(hasModifier("active:bg-blue-500")).toBe(true);
|
|
165
|
+
expect(hasModifier("hover:text-red-500")).toBe(true);
|
|
166
|
+
expect(hasModifier("focus:border-green-500")).toBe(true);
|
|
167
|
+
expect(hasModifier("disabled:bg-gray-400")).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should return false for classes without modifiers", () => {
|
|
171
|
+
expect(hasModifier("bg-blue-500")).toBe(false);
|
|
172
|
+
expect(hasModifier("text-red-500")).toBe(false);
|
|
173
|
+
expect(hasModifier("p-4")).toBe(false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should return false for unsupported modifiers", () => {
|
|
177
|
+
expect(hasModifier("selected:bg-gray-500")).toBe(false);
|
|
178
|
+
expect(hasModifier("pressed:bg-blue-500")).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("should return false for nested modifiers", () => {
|
|
182
|
+
expect(hasModifier("active:hover:bg-blue-500")).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("should return false for empty base class", () => {
|
|
186
|
+
expect(hasModifier("active:")).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("should return false for empty string", () => {
|
|
190
|
+
expect(hasModifier("")).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("splitModifierClasses - basic functionality", () => {
|
|
195
|
+
it("should split classes with and without modifiers", () => {
|
|
196
|
+
const result = splitModifierClasses("bg-blue-500 active:bg-blue-700");
|
|
197
|
+
expect(result).toEqual({
|
|
198
|
+
baseClasses: ["bg-blue-500"],
|
|
199
|
+
modifierClasses: [{ modifier: "active", baseClass: "bg-blue-700" }],
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("should handle multiple base classes", () => {
|
|
204
|
+
const result = splitModifierClasses("bg-blue-500 p-4 m-2 text-white");
|
|
205
|
+
expect(result).toEqual({
|
|
206
|
+
baseClasses: ["bg-blue-500", "p-4", "m-2", "text-white"],
|
|
207
|
+
modifierClasses: [],
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should handle multiple modifier classes", () => {
|
|
212
|
+
const result = splitModifierClasses("active:bg-blue-700 hover:bg-blue-800 focus:bg-blue-900");
|
|
213
|
+
expect(result).toEqual({
|
|
214
|
+
baseClasses: [],
|
|
215
|
+
modifierClasses: [
|
|
216
|
+
{ modifier: "active", baseClass: "bg-blue-700" },
|
|
217
|
+
{ modifier: "hover", baseClass: "bg-blue-800" },
|
|
218
|
+
{ modifier: "focus", baseClass: "bg-blue-900" },
|
|
219
|
+
],
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should handle mixed base and modifier classes", () => {
|
|
224
|
+
const result = splitModifierClasses("bg-blue-500 active:bg-blue-700 p-4 active:p-6");
|
|
225
|
+
expect(result).toEqual({
|
|
226
|
+
baseClasses: ["bg-blue-500", "p-4"],
|
|
227
|
+
modifierClasses: [
|
|
228
|
+
{ modifier: "active", baseClass: "bg-blue-700" },
|
|
229
|
+
{ modifier: "active", baseClass: "p-6" },
|
|
230
|
+
],
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("splitModifierClasses - whitespace handling", () => {
|
|
236
|
+
it("should handle leading whitespace", () => {
|
|
237
|
+
const result = splitModifierClasses(" bg-blue-500 active:bg-blue-700");
|
|
238
|
+
expect(result).toEqual({
|
|
239
|
+
baseClasses: ["bg-blue-500"],
|
|
240
|
+
modifierClasses: [{ modifier: "active", baseClass: "bg-blue-700" }],
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("should handle trailing whitespace", () => {
|
|
245
|
+
const result = splitModifierClasses("bg-blue-500 active:bg-blue-700 ");
|
|
246
|
+
expect(result).toEqual({
|
|
247
|
+
baseClasses: ["bg-blue-500"],
|
|
248
|
+
modifierClasses: [{ modifier: "active", baseClass: "bg-blue-700" }],
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("should handle multiple spaces between classes", () => {
|
|
253
|
+
const result = splitModifierClasses("bg-blue-500 active:bg-blue-700");
|
|
254
|
+
expect(result).toEqual({
|
|
255
|
+
baseClasses: ["bg-blue-500"],
|
|
256
|
+
modifierClasses: [{ modifier: "active", baseClass: "bg-blue-700" }],
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("should handle tabs and newlines", () => {
|
|
261
|
+
const result = splitModifierClasses("bg-blue-500\tactive:bg-blue-700\np-4");
|
|
262
|
+
expect(result).toEqual({
|
|
263
|
+
baseClasses: ["bg-blue-500", "p-4"],
|
|
264
|
+
modifierClasses: [{ modifier: "active", baseClass: "bg-blue-700" }],
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("should handle empty string", () => {
|
|
269
|
+
const result = splitModifierClasses("");
|
|
270
|
+
expect(result).toEqual({
|
|
271
|
+
baseClasses: [],
|
|
272
|
+
modifierClasses: [],
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("should handle whitespace-only string", () => {
|
|
277
|
+
const result = splitModifierClasses(" ");
|
|
278
|
+
expect(result).toEqual({
|
|
279
|
+
baseClasses: [],
|
|
280
|
+
modifierClasses: [],
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("splitModifierClasses - complex scenarios", () => {
|
|
286
|
+
it("should handle duplicate modifiers for same property", () => {
|
|
287
|
+
const result = splitModifierClasses("active:bg-blue-700 active:bg-red-700");
|
|
288
|
+
expect(result).toEqual({
|
|
289
|
+
baseClasses: [],
|
|
290
|
+
modifierClasses: [
|
|
291
|
+
{ modifier: "active", baseClass: "bg-blue-700" },
|
|
292
|
+
{ modifier: "active", baseClass: "bg-red-700" },
|
|
293
|
+
],
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("should handle all four modifier types", () => {
|
|
298
|
+
const result = splitModifierClasses(
|
|
299
|
+
"bg-gray-500 active:bg-blue-700 hover:bg-green-700 focus:bg-red-700 disabled:bg-gray-400",
|
|
300
|
+
);
|
|
301
|
+
expect(result).toEqual({
|
|
302
|
+
baseClasses: ["bg-gray-500"],
|
|
303
|
+
modifierClasses: [
|
|
304
|
+
{ modifier: "active", baseClass: "bg-blue-700" },
|
|
305
|
+
{ modifier: "hover", baseClass: "bg-green-700" },
|
|
306
|
+
{ modifier: "focus", baseClass: "bg-red-700" },
|
|
307
|
+
{ modifier: "disabled", baseClass: "bg-gray-400" },
|
|
308
|
+
],
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("should ignore unsupported modifiers in the base classes", () => {
|
|
313
|
+
const result = splitModifierClasses("bg-blue-500 pressed:bg-gray-500 active:bg-blue-700");
|
|
314
|
+
expect(result).toEqual({
|
|
315
|
+
baseClasses: ["bg-blue-500", "pressed:bg-gray-500"],
|
|
316
|
+
modifierClasses: [{ modifier: "active", baseClass: "bg-blue-700" }],
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("should handle modifiers with arbitrary values", () => {
|
|
321
|
+
const result = splitModifierClasses("bg-blue-500 active:bg-[#ff0000] hover:w-[200px]");
|
|
322
|
+
expect(result).toEqual({
|
|
323
|
+
baseClasses: ["bg-blue-500"],
|
|
324
|
+
modifierClasses: [
|
|
325
|
+
{ modifier: "active", baseClass: "bg-[#ff0000]" },
|
|
326
|
+
{ modifier: "hover", baseClass: "w-[200px]" },
|
|
327
|
+
],
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("should handle real-world className example", () => {
|
|
332
|
+
const result = splitModifierClasses(
|
|
333
|
+
"flex items-center justify-center bg-blue-500 p-4 active:bg-blue-700 active:scale-95 hover:bg-blue-600",
|
|
334
|
+
);
|
|
335
|
+
expect(result).toEqual({
|
|
336
|
+
baseClasses: ["flex", "items-center", "justify-center", "bg-blue-500", "p-4"],
|
|
337
|
+
modifierClasses: [
|
|
338
|
+
{ modifier: "active", baseClass: "bg-blue-700" },
|
|
339
|
+
{ modifier: "active", baseClass: "scale-95" },
|
|
340
|
+
{ modifier: "hover", baseClass: "bg-blue-600" },
|
|
341
|
+
],
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe("splitModifierClasses - nested modifiers", () => {
|
|
347
|
+
it("should ignore nested modifiers", () => {
|
|
348
|
+
const result = splitModifierClasses("bg-blue-500 active:hover:bg-red-500");
|
|
349
|
+
expect(result).toEqual({
|
|
350
|
+
baseClasses: ["bg-blue-500", "active:hover:bg-red-500"],
|
|
351
|
+
modifierClasses: [],
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe("type safety", () => {
|
|
357
|
+
it("should properly type modifier types", () => {
|
|
358
|
+
const result = parseModifier("active:bg-blue-500");
|
|
359
|
+
if (result) {
|
|
360
|
+
const modifier: ModifierType = result.modifier;
|
|
361
|
+
expect(["active", "hover", "focus", "disabled"]).toContain(modifier);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("should properly type ParsedModifier", () => {
|
|
366
|
+
const result = parseModifier("hover:text-red-500");
|
|
367
|
+
if (result) {
|
|
368
|
+
const parsed: ParsedModifier = result;
|
|
369
|
+
expect(parsed).toHaveProperty("modifier");
|
|
370
|
+
expect(parsed).toHaveProperty("baseClass");
|
|
371
|
+
expect(typeof parsed.modifier).toBe("string");
|
|
372
|
+
expect(typeof parsed.baseClass).toBe("string");
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modifier parsing utilities for state-based class names (active:, hover:, focus:)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type ModifierType = "active" | "hover" | "focus" | "disabled";
|
|
6
|
+
|
|
7
|
+
export type ParsedModifier = {
|
|
8
|
+
modifier: ModifierType;
|
|
9
|
+
baseClass: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Supported modifiers that map to component states
|
|
14
|
+
*/
|
|
15
|
+
const SUPPORTED_MODIFIERS: readonly ModifierType[] = ["active", "hover", "focus", "disabled"] as const;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse a class name to detect and extract modifiers
|
|
19
|
+
*
|
|
20
|
+
* @param cls - Class name to parse (e.g., "active:bg-blue-500")
|
|
21
|
+
* @returns ParsedModifier if modifier found, null otherwise
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* parseModifier("active:bg-blue-500") // { modifier: "active", baseClass: "bg-blue-500" }
|
|
25
|
+
* parseModifier("bg-blue-500") // null
|
|
26
|
+
* parseModifier("hover:focus:bg-blue-500") // null (nested modifiers not supported)
|
|
27
|
+
*/
|
|
28
|
+
export function parseModifier(cls: string): ParsedModifier | null {
|
|
29
|
+
const colonIndex = cls.indexOf(":");
|
|
30
|
+
|
|
31
|
+
// No colon means no modifier
|
|
32
|
+
if (colonIndex === -1) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const potentialModifier = cls.slice(0, colonIndex);
|
|
37
|
+
const baseClass = cls.slice(colonIndex + 1);
|
|
38
|
+
|
|
39
|
+
// Check if it's a supported modifier
|
|
40
|
+
if (!SUPPORTED_MODIFIERS.includes(potentialModifier as ModifierType)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check for nested modifiers (not currently supported)
|
|
45
|
+
if (baseClass.includes(":")) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Base class must not be empty
|
|
50
|
+
if (!baseClass) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
modifier: potentialModifier as ModifierType,
|
|
56
|
+
baseClass,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if a class name contains a modifier
|
|
62
|
+
*
|
|
63
|
+
* @param cls - Class name to check
|
|
64
|
+
* @returns true if class has a supported modifier prefix
|
|
65
|
+
*/
|
|
66
|
+
export function hasModifier(cls: string): boolean {
|
|
67
|
+
return parseModifier(cls) !== null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Split a space-separated className string into base and modifier classes
|
|
72
|
+
*
|
|
73
|
+
* @param className - Space-separated class names
|
|
74
|
+
* @returns Object with baseClasses and modifierClasses arrays
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* splitModifierClasses("bg-blue-500 active:bg-blue-700 p-4 active:p-6")
|
|
78
|
+
* // {
|
|
79
|
+
* // baseClasses: ["bg-blue-500", "p-4"],
|
|
80
|
+
* // modifierClasses: [
|
|
81
|
+
* // { modifier: "active", baseClass: "bg-blue-700" },
|
|
82
|
+
* // { modifier: "active", baseClass: "p-6" }
|
|
83
|
+
* // ]
|
|
84
|
+
* // }
|
|
85
|
+
*/
|
|
86
|
+
export function splitModifierClasses(className: string): {
|
|
87
|
+
baseClasses: string[];
|
|
88
|
+
modifierClasses: ParsedModifier[];
|
|
89
|
+
} {
|
|
90
|
+
const classes = className.trim().split(/\s+/).filter(Boolean);
|
|
91
|
+
const baseClasses: string[] = [];
|
|
92
|
+
const modifierClasses: ParsedModifier[] = [];
|
|
93
|
+
|
|
94
|
+
for (const cls of classes) {
|
|
95
|
+
const parsed = parseModifier(cls);
|
|
96
|
+
if (parsed) {
|
|
97
|
+
modifierClasses.push(parsed);
|
|
98
|
+
} else {
|
|
99
|
+
baseClasses.push(cls);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { baseClasses, modifierClasses };
|
|
104
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { setPlatform } from "test/mocks/react-native";
|
|
2
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { SHADOW_SCALE, parseShadow, rebuildShadowScale } from "./shadows";
|
|
4
|
+
|
|
5
|
+
// Reset to iOS before each test
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
setPlatform("ios");
|
|
8
|
+
rebuildShadowScale();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe("SHADOW_SCALE", () => {
|
|
12
|
+
it("should export complete shadow scale", () => {
|
|
13
|
+
expect(SHADOW_SCALE).toMatchSnapshot();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should have all shadow variants", () => {
|
|
17
|
+
expect(SHADOW_SCALE).toHaveProperty("shadow-sm");
|
|
18
|
+
expect(SHADOW_SCALE).toHaveProperty("shadow");
|
|
19
|
+
expect(SHADOW_SCALE).toHaveProperty("shadow-md");
|
|
20
|
+
expect(SHADOW_SCALE).toHaveProperty("shadow-lg");
|
|
21
|
+
expect(SHADOW_SCALE).toHaveProperty("shadow-xl");
|
|
22
|
+
expect(SHADOW_SCALE).toHaveProperty("shadow-2xl");
|
|
23
|
+
expect(SHADOW_SCALE).toHaveProperty("shadow-none");
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("parseShadow - basic shadows", () => {
|
|
28
|
+
it("should parse shadow-sm", () => {
|
|
29
|
+
const result = parseShadow("shadow-sm");
|
|
30
|
+
expect(result).toBeTruthy();
|
|
31
|
+
expect(result).toHaveProperty("shadowColor");
|
|
32
|
+
expect(result).toHaveProperty("shadowOffset");
|
|
33
|
+
expect(result).toHaveProperty("shadowOpacity");
|
|
34
|
+
expect(result).toHaveProperty("shadowRadius");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should parse default shadow", () => {
|
|
38
|
+
const result = parseShadow("shadow");
|
|
39
|
+
expect(result).toBeTruthy();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should parse shadow-md", () => {
|
|
43
|
+
const result = parseShadow("shadow-md");
|
|
44
|
+
expect(result).toBeTruthy();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should parse shadow-lg", () => {
|
|
48
|
+
const result = parseShadow("shadow-lg");
|
|
49
|
+
expect(result).toBeTruthy();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should parse shadow-xl", () => {
|
|
53
|
+
const result = parseShadow("shadow-xl");
|
|
54
|
+
expect(result).toBeTruthy();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should parse shadow-2xl", () => {
|
|
58
|
+
const result = parseShadow("shadow-2xl");
|
|
59
|
+
expect(result).toBeTruthy();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should parse shadow-none", () => {
|
|
63
|
+
const result = parseShadow("shadow-none");
|
|
64
|
+
expect(result).toBeTruthy();
|
|
65
|
+
expect(result).toMatchObject({
|
|
66
|
+
shadowColor: "transparent",
|
|
67
|
+
shadowOpacity: 0,
|
|
68
|
+
shadowRadius: 0,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("parseShadow - shadow properties (iOS)", () => {
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
setPlatform("ios");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should have increasing shadow values for larger shadows", () => {
|
|
79
|
+
const sm = parseShadow("shadow-sm");
|
|
80
|
+
const md = parseShadow("shadow-md");
|
|
81
|
+
const lg = parseShadow("shadow-lg");
|
|
82
|
+
const xl = parseShadow("shadow-xl");
|
|
83
|
+
const xxl = parseShadow("shadow-2xl");
|
|
84
|
+
|
|
85
|
+
// Shadow opacity should increase
|
|
86
|
+
expect(md?.shadowOpacity).toBeGreaterThan(sm?.shadowOpacity as number);
|
|
87
|
+
expect(lg?.shadowOpacity).toBeGreaterThan(md?.shadowOpacity as number);
|
|
88
|
+
|
|
89
|
+
// Shadow radius should increase
|
|
90
|
+
expect(md?.shadowRadius).toBeGreaterThan(sm?.shadowRadius as number);
|
|
91
|
+
expect(lg?.shadowRadius).toBeGreaterThan(md?.shadowRadius as number);
|
|
92
|
+
expect(xl?.shadowRadius).toBeGreaterThan(lg?.shadowRadius as number);
|
|
93
|
+
expect(xxl?.shadowRadius).toBeGreaterThan(xl?.shadowRadius as number);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should use consistent shadow color", () => {
|
|
97
|
+
const shadows = ["shadow-sm", "shadow", "shadow-md", "shadow-lg", "shadow-xl", "shadow-2xl"];
|
|
98
|
+
|
|
99
|
+
shadows.forEach((shadow) => {
|
|
100
|
+
const result = parseShadow(shadow);
|
|
101
|
+
expect(result?.shadowColor).toBe("#000000");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should have proper shadowOffset structure", () => {
|
|
106
|
+
const result = parseShadow("shadow");
|
|
107
|
+
expect(result?.shadowOffset).toHaveProperty("width");
|
|
108
|
+
expect(result?.shadowOffset).toHaveProperty("height");
|
|
109
|
+
expect(typeof result?.shadowOffset?.width).toBe("number");
|
|
110
|
+
expect(typeof result?.shadowOffset?.height).toBe("number");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should not have elevation property on iOS", () => {
|
|
114
|
+
const result = parseShadow("shadow");
|
|
115
|
+
expect(result).not.toHaveProperty("elevation");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("parseShadow - shadow properties (Android)", () => {
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
setPlatform("android");
|
|
122
|
+
rebuildShadowScale();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should have increasing elevation values for larger shadows", () => {
|
|
126
|
+
const sm = parseShadow("shadow-sm");
|
|
127
|
+
const md = parseShadow("shadow-md");
|
|
128
|
+
const lg = parseShadow("shadow-lg");
|
|
129
|
+
const xl = parseShadow("shadow-xl");
|
|
130
|
+
const xxl = parseShadow("shadow-2xl");
|
|
131
|
+
|
|
132
|
+
// Elevation should increase
|
|
133
|
+
expect(md?.elevation).toBeGreaterThan(sm?.elevation as number);
|
|
134
|
+
expect(lg?.elevation).toBeGreaterThan(md?.elevation as number);
|
|
135
|
+
expect(xl?.elevation).toBeGreaterThan(lg?.elevation as number);
|
|
136
|
+
expect(xxl?.elevation).toBeGreaterThan(xl?.elevation as number);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should not have shadow properties on Android", () => {
|
|
140
|
+
const result = parseShadow("shadow");
|
|
141
|
+
expect(result).not.toHaveProperty("shadowColor");
|
|
142
|
+
expect(result).not.toHaveProperty("shadowOffset");
|
|
143
|
+
expect(result).not.toHaveProperty("shadowOpacity");
|
|
144
|
+
expect(result).not.toHaveProperty("shadowRadius");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("parseShadow - invalid classes", () => {
|
|
149
|
+
it("should return null for invalid shadow classes", () => {
|
|
150
|
+
expect(parseShadow("shadow-invalid")).toBeNull();
|
|
151
|
+
expect(parseShadow("shadows")).toBeNull();
|
|
152
|
+
expect(parseShadow("shadow-")).toBeNull();
|
|
153
|
+
expect(parseShadow("shadow-small")).toBeNull();
|
|
154
|
+
expect(parseShadow("shadow-3xl")).toBeNull();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should return null for non-shadow classes", () => {
|
|
158
|
+
expect(parseShadow("bg-blue-500")).toBeNull();
|
|
159
|
+
expect(parseShadow("p-4")).toBeNull();
|
|
160
|
+
expect(parseShadow("text-white")).toBeNull();
|
|
161
|
+
expect(parseShadow("border-2")).toBeNull();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("should return null for empty or invalid input", () => {
|
|
165
|
+
expect(parseShadow("")).toBeNull();
|
|
166
|
+
expect(parseShadow("shadow123")).toBeNull();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("parseShadow - comprehensive coverage", () => {
|
|
171
|
+
it("should parse all shadow variants without errors", () => {
|
|
172
|
+
const variants = [
|
|
173
|
+
"shadow-sm",
|
|
174
|
+
"shadow",
|
|
175
|
+
"shadow-md",
|
|
176
|
+
"shadow-lg",
|
|
177
|
+
"shadow-xl",
|
|
178
|
+
"shadow-2xl",
|
|
179
|
+
"shadow-none",
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
variants.forEach((variant) => {
|
|
183
|
+
const result = parseShadow(variant);
|
|
184
|
+
expect(result).toBeTruthy();
|
|
185
|
+
expect(typeof result).toBe("object");
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("should return consistent results for same input", () => {
|
|
190
|
+
const result1 = parseShadow("shadow-md");
|
|
191
|
+
const result2 = parseShadow("shadow-md");
|
|
192
|
+
expect(result1).toEqual(result2);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("should handle case-sensitive class names", () => {
|
|
196
|
+
// Shadow classes are case-sensitive
|
|
197
|
+
expect(parseShadow("SHADOW")).toBeNull();
|
|
198
|
+
expect(parseShadow("Shadow-md")).toBeNull();
|
|
199
|
+
expect(parseShadow("shadow-MD")).toBeNull();
|
|
200
|
+
});
|
|
201
|
+
});
|