@mgcrea/react-native-tailwind 0.12.0 → 0.13.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 (97) hide show
  1. package/README.md +29 -2014
  2. package/dist/babel/config-loader.d.ts +3 -0
  3. package/dist/babel/config-loader.test.ts +2 -2
  4. package/dist/babel/config-loader.ts +37 -2
  5. package/dist/babel/index.cjs +2855 -2434
  6. package/dist/babel/plugin/componentScope.d.ts +26 -0
  7. package/dist/babel/plugin/componentScope.ts +87 -0
  8. package/dist/babel/plugin/state.d.ts +119 -0
  9. package/dist/babel/plugin/state.ts +177 -0
  10. package/dist/babel/plugin/visitors/className.d.ts +11 -0
  11. package/{src/babel/plugin.test.ts → dist/babel/plugin/visitors/className.test.ts} +74 -674
  12. package/dist/babel/plugin/visitors/className.ts +624 -0
  13. package/dist/babel/plugin/visitors/className.windowDimensions.test.ts +406 -0
  14. package/dist/babel/plugin/visitors/imports.d.ts +11 -0
  15. package/dist/babel/plugin/visitors/imports.test.ts +88 -0
  16. package/dist/babel/plugin/visitors/imports.ts +101 -0
  17. package/dist/babel/plugin/visitors/program.d.ts +15 -0
  18. package/dist/babel/plugin/visitors/program.test.ts +325 -0
  19. package/dist/babel/plugin/visitors/program.ts +99 -0
  20. package/dist/babel/plugin/visitors/tw.d.ts +16 -0
  21. package/dist/babel/plugin/visitors/tw.test.ts +620 -0
  22. package/dist/babel/plugin/visitors/tw.ts +148 -0
  23. package/dist/babel/plugin.d.ts +3 -96
  24. package/dist/babel/plugin.test.ts +470 -0
  25. package/dist/babel/plugin.ts +28 -953
  26. package/dist/babel/utils/colorSchemeModifierProcessing.ts +11 -0
  27. package/dist/babel/utils/componentSupport.test.ts +20 -7
  28. package/dist/babel/utils/componentSupport.ts +2 -0
  29. package/dist/babel/utils/modifierProcessing.ts +21 -0
  30. package/dist/babel/utils/platformModifierProcessing.ts +11 -0
  31. package/dist/babel/utils/styleInjection.d.ts +15 -0
  32. package/dist/babel/utils/styleInjection.ts +172 -17
  33. package/dist/babel/utils/twProcessing.ts +11 -0
  34. package/dist/babel/utils/windowDimensionsProcessing.d.ts +56 -0
  35. package/dist/babel/utils/windowDimensionsProcessing.ts +121 -0
  36. package/dist/components/TouchableOpacity.d.ts +35 -0
  37. package/dist/components/TouchableOpacity.js +1 -0
  38. package/dist/components/index.d.ts +3 -0
  39. package/dist/components/index.js +1 -0
  40. package/dist/config/markers.d.ts +5 -0
  41. package/dist/config/markers.js +1 -0
  42. package/dist/index.d.ts +2 -5
  43. package/dist/index.js +1 -1
  44. package/dist/parser/borders.d.ts +3 -1
  45. package/dist/parser/borders.js +1 -1
  46. package/dist/parser/borders.test.js +1 -1
  47. package/dist/parser/colors.js +1 -1
  48. package/dist/parser/colors.test.js +1 -1
  49. package/dist/parser/index.d.ts +1 -0
  50. package/dist/parser/index.js +1 -1
  51. package/dist/parser/layout.js +1 -1
  52. package/dist/parser/layout.test.js +1 -1
  53. package/dist/parser/sizing.js +1 -1
  54. package/dist/parser/typography.d.ts +2 -1
  55. package/dist/parser/typography.js +1 -1
  56. package/dist/parser/typography.test.js +1 -1
  57. package/dist/runtime.cjs +1 -1
  58. package/dist/runtime.cjs.map +4 -4
  59. package/dist/runtime.js +1 -1
  60. package/dist/runtime.js.map +4 -4
  61. package/package.json +1 -1
  62. package/src/babel/config-loader.test.ts +2 -2
  63. package/src/babel/config-loader.ts +37 -2
  64. package/src/babel/plugin/componentScope.ts +87 -0
  65. package/src/babel/plugin/state.ts +177 -0
  66. package/src/babel/plugin/visitors/className.test.ts +1312 -0
  67. package/src/babel/plugin/visitors/className.ts +624 -0
  68. package/src/babel/plugin/visitors/className.windowDimensions.test.ts +406 -0
  69. package/src/babel/plugin/visitors/imports.test.ts +88 -0
  70. package/src/babel/plugin/visitors/imports.ts +101 -0
  71. package/src/babel/plugin/visitors/program.test.ts +325 -0
  72. package/src/babel/plugin/visitors/program.ts +99 -0
  73. package/src/babel/plugin/visitors/tw.test.ts +620 -0
  74. package/src/babel/plugin/visitors/tw.ts +148 -0
  75. package/src/babel/plugin.ts +28 -953
  76. package/src/babel/utils/colorSchemeModifierProcessing.ts +11 -0
  77. package/src/babel/utils/componentSupport.test.ts +20 -7
  78. package/src/babel/utils/componentSupport.ts +2 -0
  79. package/src/babel/utils/modifierProcessing.ts +21 -0
  80. package/src/babel/utils/platformModifierProcessing.ts +11 -0
  81. package/src/babel/utils/styleInjection.ts +172 -17
  82. package/src/babel/utils/twProcessing.ts +11 -0
  83. package/src/babel/utils/windowDimensionsProcessing.ts +121 -0
  84. package/src/components/TouchableOpacity.tsx +71 -0
  85. package/src/components/index.ts +3 -0
  86. package/src/config/markers.ts +5 -0
  87. package/src/index.ts +4 -5
  88. package/src/parser/borders.test.ts +58 -0
  89. package/src/parser/borders.ts +18 -3
  90. package/src/parser/colors.test.ts +249 -0
  91. package/src/parser/colors.ts +38 -0
  92. package/src/parser/index.ts +4 -3
  93. package/src/parser/layout.test.ts +61 -0
  94. package/src/parser/layout.ts +55 -1
  95. package/src/parser/sizing.ts +11 -0
  96. package/src/parser/typography.test.ts +102 -0
  97. package/src/parser/typography.ts +61 -15
@@ -0,0 +1,1312 @@
1
+ /* eslint-disable @typescript-eslint/no-empty-function */
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { transform } from "../../../../test/helpers/babelTransform.js";
4
+
5
+ describe("className visitor - basic transformation", () => {
6
+ it("should still transform className props", () => {
7
+ const input = `
8
+ import { View } from 'react-native';
9
+ export function Component() {
10
+ return <View className="m-4 p-2 bg-blue-500" />;
11
+ }
12
+ `;
13
+
14
+ const output = transform(input, undefined, true); // Enable JSX
15
+
16
+ // Should have StyleSheet
17
+ expect(output).toContain("StyleSheet.create");
18
+ expect(output).toContain("_twStyles");
19
+
20
+ // Should replace className with style
21
+ expect(output).not.toContain("className");
22
+ expect(output).toContain("style:");
23
+ });
24
+
25
+ it("should work with both tw and className in same file", () => {
26
+ const input = `
27
+ import { tw } from '@mgcrea/react-native-tailwind';
28
+ import { View } from 'react-native';
29
+
30
+ const styles = tw\`bg-red-500\`;
31
+
32
+ export function Component() {
33
+ return <View className="m-4 p-2" />;
34
+ }
35
+ `;
36
+
37
+ const output = transform(input, undefined, true); // Enable JSX
38
+
39
+ // Should have both styles in StyleSheet
40
+ expect(output).toContain("_bg_red_500");
41
+ expect(output).toContain("_m_4_p_2");
42
+ });
43
+
44
+ it("should merge className with function-based style prop", () => {
45
+ const input = `
46
+ import { TextInput } from 'react-native';
47
+ export function Component() {
48
+ return (
49
+ <TextInput
50
+ className="border border-gray-300 bg-gray-100"
51
+ style={({ focused, disabled }) => [
52
+ baseStyles,
53
+ focused && focusedStyles,
54
+ ]}
55
+ />
56
+ );
57
+ }
58
+ `;
59
+
60
+ const output = transform(input, undefined, true); // Enable JSX
61
+
62
+ // Should have StyleSheet with className styles
63
+ expect(output).toContain("StyleSheet.create");
64
+ // Style keys are sorted alphabetically: bg-gray-100 comes before border
65
+ expect(output).toContain("_bg_gray_100_border_border_gray_300");
66
+
67
+ // Should create a wrapper function that merges both
68
+ // The wrapper should call the original function and merge results
69
+ expect(output).toContain("_state");
70
+ expect(output).toContain("_twStyles._bg_gray_100_border_border_gray_300");
71
+
72
+ // Should not have className in output
73
+ expect(output).not.toContain("className");
74
+
75
+ // Should have a function that accepts state and returns an array
76
+ expect(output).toMatch(/_state\s*=>/);
77
+ });
78
+
79
+ it("should merge dynamic className with function-based style prop", () => {
80
+ const input = `
81
+ import { TextInput } from 'react-native';
82
+ export function Component({ isError }) {
83
+ return (
84
+ <TextInput
85
+ className={\`border \${isError ? 'border-red-500' : 'border-gray-300'}\`}
86
+ style={({ focused }) => [
87
+ baseStyles,
88
+ focused && focusedStyles,
89
+ ]}
90
+ />
91
+ );
92
+ }
93
+ `;
94
+
95
+ const output = transform(input, undefined, true); // Enable JSX
96
+
97
+ // Should have StyleSheet with both className styles
98
+ expect(output).toContain("StyleSheet.create");
99
+ expect(output).toContain("_border");
100
+ expect(output).toContain("_border_red_500");
101
+ expect(output).toContain("_border_gray_300");
102
+
103
+ // Should create a wrapper function that merges dynamic styles with function result
104
+ expect(output).toContain("_state");
105
+
106
+ // Should not have className in output
107
+ expect(output).not.toContain("className");
108
+ });
109
+
110
+ it('should transform className={"..."} (string literal in expression container)', () => {
111
+ const input = `
112
+ import { View } from 'react-native';
113
+ export function Component() {
114
+ return <View className={"flex-row items-center justify-start"} />;
115
+ }
116
+ `;
117
+
118
+ const output = transform(input, undefined, true);
119
+
120
+ // Should have StyleSheet
121
+ expect(output).toContain("StyleSheet.create");
122
+ expect(output).toContain("_twStyles");
123
+
124
+ // Should replace className with style
125
+ expect(output).not.toContain("className");
126
+ expect(output).toContain("style:");
127
+
128
+ // Should have the expected style keys
129
+ expect(output).toContain("_flex_row_items_center_justify_start");
130
+ });
131
+
132
+ it('should transform className={"..."} with modifiers', () => {
133
+ const input = `
134
+ import { Pressable } from 'react-native';
135
+ export function Component() {
136
+ return <Pressable className={"bg-blue-500 active:bg-blue-700 p-4"} />;
137
+ }
138
+ `;
139
+
140
+ const output = transform(input, undefined, true);
141
+
142
+ // Should have StyleSheet with both base and active styles
143
+ expect(output).toContain("_bg_blue_500_p_4");
144
+ expect(output).toContain("_active_bg_blue_700");
145
+
146
+ // Should have style function for active modifier (Pressable uses 'pressed' parameter)
147
+ expect(output).toMatch(/(pressed|_state)/);
148
+
149
+ // Should not have className in output
150
+ expect(output).not.toContain("className");
151
+ });
152
+
153
+ it('should transform className={"..."} with platform modifiers', () => {
154
+ const input = `
155
+ import { View } from 'react-native';
156
+ export function Component() {
157
+ return <View className={"p-4 ios:p-6 android:p-8"} />;
158
+ }
159
+ `;
160
+
161
+ const output = transform(input, undefined, true);
162
+
163
+ // Should have Platform import
164
+ expect(output).toContain("Platform");
165
+ expect(output).toMatch(/from ['"]react-native['"]/); // Match both single and double quotes
166
+
167
+ // Should have Platform.select
168
+ expect(output).toContain("Platform.select");
169
+
170
+ // Should have platform-specific styles
171
+ expect(output).toContain("_ios_p_6");
172
+ expect(output).toContain("_android_p_8");
173
+
174
+ // Should not have className in output
175
+ expect(output).not.toContain("className");
176
+ });
177
+
178
+ it('should handle empty className={""}', () => {
179
+ const input = `
180
+ import { View } from 'react-native';
181
+ export function Component() {
182
+ return <View className={""} />;
183
+ }
184
+ `;
185
+
186
+ const output = transform(input, undefined, true);
187
+
188
+ // Should remove empty className attribute entirely
189
+ expect(output).not.toContain("className");
190
+ expect(output).not.toContain("style=");
191
+ });
192
+ });
193
+
194
+ describe("className visitor - placeholder: modifier", () => {
195
+ it("should transform placeholder:text-{color} to placeholderTextColor prop", () => {
196
+ const input = `
197
+ import { TextInput } from 'react-native';
198
+ export function Component() {
199
+ return (
200
+ <TextInput
201
+ className="border-2 placeholder:text-gray-400"
202
+ placeholder="Email"
203
+ />
204
+ );
205
+ }
206
+ `;
207
+
208
+ const output = transform(input, undefined, true);
209
+
210
+ // Should have placeholderTextColor prop with correct hex value (from custom palette)
211
+ expect(output).toContain('placeholderTextColor: "#99a1af"');
212
+
213
+ // Should still have style for border-2
214
+ expect(output).toContain("StyleSheet.create");
215
+ expect(output).toContain("_border_2");
216
+
217
+ // Should not have className in output
218
+ expect(output).not.toContain("className");
219
+ });
220
+
221
+ it("should support placeholder colors with opacity", () => {
222
+ const input = `
223
+ import { TextInput } from 'react-native';
224
+ export function Component() {
225
+ return <TextInput className="placeholder:text-red-500/50" />;
226
+ }
227
+ `;
228
+
229
+ const output = transform(input, undefined, true);
230
+
231
+ // Should have 8-digit hex with alpha channel (custom palette red-500, uppercased)
232
+ expect(output).toContain('placeholderTextColor: "#FB2C3680"');
233
+ });
234
+
235
+ it("should support arbitrary placeholder colors", () => {
236
+ const input = `
237
+ import { TextInput } from 'react-native';
238
+ export function Component() {
239
+ return <TextInput className="placeholder:text-[#ff0000]" />;
240
+ }
241
+ `;
242
+
243
+ const output = transform(input, undefined, true);
244
+
245
+ expect(output).toContain('placeholderTextColor: "#ff0000"');
246
+ });
247
+
248
+ it("should combine placeholder: with other modifiers", () => {
249
+ const input = `
250
+ import { TextInput } from 'react-native';
251
+ export function Component() {
252
+ return (
253
+ <TextInput
254
+ className="border-2 focus:border-blue-500 placeholder:text-gray-400"
255
+ placeholder="Email"
256
+ />
257
+ );
258
+ }
259
+ `;
260
+
261
+ const output = transform(input, undefined, true);
262
+
263
+ // Should have placeholderTextColor prop (custom palette gray-400)
264
+ expect(output).toContain('placeholderTextColor: "#99a1af"');
265
+
266
+ // Should have focus: modifier handling (style function)
267
+ expect(output).toContain("focused");
268
+ expect(output).toMatch(/style[\s\S]*=>/); // Style function
269
+
270
+ // Should not have className
271
+ expect(output).not.toContain("className");
272
+ });
273
+
274
+ it("should handle multiple placeholder: classes (last wins)", () => {
275
+ const input = `
276
+ import { TextInput } from 'react-native';
277
+ export function Component() {
278
+ return (
279
+ <TextInput className="placeholder:text-red-500 placeholder:text-blue-500" />
280
+ );
281
+ }
282
+ `;
283
+
284
+ const output = transform(input, undefined, true);
285
+
286
+ // Blue should win (last color, custom palette blue-500)
287
+ expect(output).toContain('placeholderTextColor: "#2b7fff"');
288
+ });
289
+
290
+ it("should ignore non-text utilities in placeholder: modifier", () => {
291
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
292
+
293
+ const input = `
294
+ import { TextInput } from 'react-native';
295
+ export function Component() {
296
+ return (
297
+ <TextInput className="placeholder:font-bold placeholder:text-gray-400" />
298
+ );
299
+ }
300
+ `;
301
+
302
+ const output = transform(input, undefined, true);
303
+
304
+ // Should still have the valid text color (custom palette gray-400)
305
+ expect(output).toContain('placeholderTextColor: "#99a1af"');
306
+
307
+ // Should not have font-bold anywhere
308
+ expect(output).not.toContain("fontWeight");
309
+
310
+ consoleSpy.mockRestore();
311
+ });
312
+
313
+ it.skip("should work with custom colors", () => {
314
+ // Note: This test would require setting up a tailwind.config file
315
+ // For now, we'll skip custom color testing in Babel tests
316
+ // Custom colors are tested in the parser tests
317
+ });
318
+
319
+ it("should not transform placeholder: on non-TextInput elements", () => {
320
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
321
+
322
+ const input = `
323
+ import { View } from 'react-native';
324
+ export function Component() {
325
+ return <View className="placeholder:text-gray-400" />;
326
+ }
327
+ `;
328
+
329
+ const output = transform(input, undefined, true);
330
+
331
+ // Should not have placeholderTextColor prop (View doesn't support it)
332
+ expect(output).not.toContain("placeholderTextColor");
333
+
334
+ // Should warn about unsupported modifier
335
+ // (The warning happens because View doesn't support any modifiers)
336
+
337
+ consoleSpy.mockRestore();
338
+ });
339
+ });
340
+
341
+ describe("className visitor - platform modifiers", () => {
342
+ it("should transform platform modifiers to Platform.select()", () => {
343
+ const input = `
344
+ import React from 'react';
345
+ import { View } from 'react-native';
346
+
347
+ export function Component() {
348
+ return (
349
+ <View className="p-4 ios:p-6 android:p-8" />
350
+ );
351
+ }
352
+ `;
353
+
354
+ const output = transform(input, undefined, true);
355
+
356
+ // Should import Platform from react-native
357
+ expect(output).toContain("Platform");
358
+ expect(output).toMatch(/import.*Platform.*from ['"]react-native['"]/);
359
+
360
+ // Should generate Platform.select()
361
+ expect(output).toContain("Platform.select");
362
+
363
+ // Should have base padding style
364
+ expect(output).toContain("_p_4");
365
+
366
+ // Should have iOS and Android specific styles
367
+ expect(output).toContain("_ios_p_6");
368
+ expect(output).toContain("_android_p_8");
369
+
370
+ // Should have correct style values in StyleSheet.create
371
+ expect(output).toMatch(/padding:\s*16/); // p-4
372
+ expect(output).toMatch(/padding:\s*24/); // p-6 (ios)
373
+ expect(output).toMatch(/padding:\s*32/); // p-8 (android)
374
+ });
375
+
376
+ it("should support multiple platform modifiers on same element", () => {
377
+ const input = `
378
+ import React from 'react';
379
+ import { View } from 'react-native';
380
+
381
+ export function Component() {
382
+ return (
383
+ <View className="bg-white ios:bg-blue-50 android:bg-green-50 p-4 ios:p-6 android:p-8" />
384
+ );
385
+ }
386
+ `;
387
+
388
+ const output = transform(input, undefined, true);
389
+
390
+ // Should have Platform import
391
+ expect(output).toContain("Platform");
392
+
393
+ // Should have base styles (combined key)
394
+ expect(output).toContain("_bg_white_p_4");
395
+
396
+ // Should have iOS specific styles (combined key for multiple ios: modifiers)
397
+ expect(output).toContain("_ios_bg_blue_50_p_6");
398
+
399
+ // Should have Android specific styles (combined key for multiple android: modifiers)
400
+ expect(output).toContain("_android_bg_green_50_p_8");
401
+
402
+ // Should contain Platform.select with both platforms
403
+ expect(output).toMatch(/Platform\.select\s*\(\s*\{[\s\S]*ios:/);
404
+ expect(output).toMatch(/Platform\.select\s*\(\s*\{[\s\S]*android:/);
405
+ });
406
+
407
+ it("should support web platform modifier", () => {
408
+ const input = `
409
+ import React from 'react';
410
+ import { View } from 'react-native';
411
+
412
+ export function Component() {
413
+ return (
414
+ <View className="p-4 web:p-2" />
415
+ );
416
+ }
417
+ `;
418
+
419
+ const output = transform(input, undefined, true);
420
+
421
+ // Should have Platform.select with web
422
+ expect(output).toContain("Platform.select");
423
+ expect(output).toContain("web:");
424
+ expect(output).toContain("_web_p_2");
425
+ });
426
+
427
+ it("should work with platform modifiers on all components", () => {
428
+ const input = `
429
+ import React from 'react';
430
+ import { View, Text, ScrollView } from 'react-native';
431
+
432
+ export function Component() {
433
+ return (
434
+ <View className="ios:bg-blue-500 android:bg-green-500">
435
+ <Text className="ios:text-lg android:text-xl">Platform text</Text>
436
+ <ScrollView contentContainerClassName="ios:p-4 android:p-8" />
437
+ </View>
438
+ );
439
+ }
440
+ `;
441
+
442
+ const output = transform(input, undefined, true);
443
+
444
+ // Should work on View - check for Platform.select separately (not checking style= format)
445
+ expect(output).toContain("Platform.select");
446
+
447
+ // Should work on Text
448
+ expect(output).toContain("_ios_text_lg");
449
+ expect(output).toContain("_android_text_xl");
450
+
451
+ // Should work on ScrollView contentContainerStyle
452
+ expect(output).toContain("contentContainerStyle");
453
+ });
454
+
455
+ it("should combine platform modifiers with state modifiers", () => {
456
+ const input = `
457
+ import React from 'react';
458
+ import { Pressable, Text } from 'react-native';
459
+
460
+ export function Component() {
461
+ return (
462
+ <Pressable className="bg-blue-500 active:bg-blue-700 ios:shadow-md android:shadow-sm p-4">
463
+ <Text className="text-white">Button</Text>
464
+ </Pressable>
465
+ );
466
+ }
467
+ `;
468
+
469
+ const output = transform(input, undefined, true);
470
+
471
+ // Should have Platform.select for platform modifiers
472
+ expect(output).toContain("Platform.select");
473
+ expect(output).toContain("_ios_shadow_md");
474
+ expect(output).toContain("_android_shadow_sm");
475
+
476
+ // Should have state modifier function for active
477
+ expect(output).toMatch(/\(\s*\{\s*pressed\s*\}\s*\)\s*=>/);
478
+ expect(output).toContain("pressed");
479
+ expect(output).toContain("_active_bg_blue_700");
480
+
481
+ // Should have base styles
482
+ expect(output).toContain("_bg_blue_500");
483
+ expect(output).toContain("_p_4");
484
+ });
485
+
486
+ it("should handle platform-specific colors", () => {
487
+ const input = `
488
+ import React from 'react';
489
+ import { View, Text } from 'react-native';
490
+
491
+ export function Component() {
492
+ return (
493
+ <View className="bg-gray-100 ios:bg-blue-50 android:bg-green-50">
494
+ <Text className="text-gray-900 ios:text-blue-900 android:text-green-900">
495
+ Platform colors
496
+ </Text>
497
+ </View>
498
+ );
499
+ }
500
+ `;
501
+
502
+ const output = transform(input, undefined, true);
503
+
504
+ // Should have color values in StyleSheet
505
+ expect(output).toMatch(/#[0-9A-F]{6}/i); // Hex color format
506
+
507
+ // Should have platform-specific color classes
508
+ expect(output).toContain("_ios_text_blue_900");
509
+ expect(output).toContain("_android_text_green_900");
510
+ });
511
+
512
+ it("should only add Platform import once when needed", () => {
513
+ const input = `
514
+ import React from 'react';
515
+ import { View } from 'react-native';
516
+
517
+ export function Component() {
518
+ return (
519
+ <>
520
+ <View className="ios:p-4" />
521
+ <View className="android:p-8" />
522
+ <View className="ios:bg-blue-500" />
523
+ </>
524
+ );
525
+ }
526
+ `;
527
+
528
+ const output = transform(input, undefined, true);
529
+
530
+ // Should have Platform import
531
+ expect(output).toContain("Platform");
532
+
533
+ // Count how many times Platform is imported (should be once)
534
+ const platformImports = output.match(/import.*Platform.*from ['"]react-native['"]/g);
535
+ expect(platformImports).toHaveLength(1);
536
+ });
537
+
538
+ it("should merge with existing Platform import", () => {
539
+ const input = `
540
+ import React from 'react';
541
+ import { View, Platform } from 'react-native';
542
+
543
+ export function Component() {
544
+ return <View className="ios:p-4 android:p-8" />;
545
+ }
546
+ `;
547
+
548
+ const output = transform(input, undefined, true);
549
+
550
+ // Should still use Platform.select
551
+ expect(output).toContain("Platform.select");
552
+
553
+ // Should not duplicate Platform import - Platform appears in import and Platform.select calls
554
+ expect(output).toMatch(/Platform.*react-native/);
555
+ });
556
+
557
+ it("should handle platform modifiers without base classes", () => {
558
+ const input = `
559
+ import React from 'react';
560
+ import { View } from 'react-native';
561
+
562
+ export function Component() {
563
+ return <View className="ios:p-6 android:p-8" />;
564
+ }
565
+ `;
566
+
567
+ const output = transform(input, undefined, true);
568
+
569
+ // Should only have Platform.select, no base style
570
+ expect(output).toContain("Platform.select");
571
+ expect(output).toContain("_ios_p_6");
572
+ expect(output).toContain("_android_p_8");
573
+
574
+ // Should not have generic padding without platform prefix
575
+ // Check that non-platform-prefixed style keys don't exist
576
+ expect(output).not.toMatch(/(?<!_ios|_android|_web)_p_4:/);
577
+ expect(output).not.toMatch(/(?<!_ios|_android|_web)_p_6:/);
578
+ expect(output).not.toMatch(/(?<!_ios|_android|_web)_p_8:/);
579
+ });
580
+ });
581
+
582
+ describe("className visitor - color scheme modifiers", () => {
583
+ it("should transform dark: modifier to conditional expression", () => {
584
+ const input = `
585
+ import React from 'react';
586
+ import { View } from 'react-native';
587
+
588
+ export function Component() {
589
+ return (
590
+ <View className="bg-white dark:bg-gray-900" />
591
+ );
592
+ }
593
+ `;
594
+
595
+ const output = transform(input, undefined, true);
596
+
597
+ // Should import useColorScheme
598
+ expect(output).toContain("useColorScheme");
599
+ expect(output).toMatch(/import.*useColorScheme.*from ['"]react-native['"]/);
600
+
601
+ // Should inject colorScheme hook in component
602
+ expect(output).toContain("_twColorScheme");
603
+ expect(output).toContain("useColorScheme()");
604
+
605
+ // Should have base bg-white style
606
+ expect(output).toContain("_bg_white");
607
+
608
+ // Should have dark:bg-gray-900 style
609
+ expect(output).toContain("_dark_bg_gray_900");
610
+
611
+ // Should generate conditional: _twColorScheme === 'dark' && ...
612
+ expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
613
+ });
614
+
615
+ it("should support both dark: and light: modifiers", () => {
616
+ const input = `
617
+ import React from 'react';
618
+ import { View } from 'react-native';
619
+
620
+ export function Component() {
621
+ return (
622
+ <View className="bg-gray-100 dark:bg-gray-900 light:bg-white" />
623
+ );
624
+ }
625
+ `;
626
+
627
+ const output = transform(input, undefined, true);
628
+
629
+ // Should have all three styles
630
+ expect(output).toContain("_bg_gray_100");
631
+ expect(output).toContain("_dark_bg_gray_900");
632
+ expect(output).toContain("_light_bg_white");
633
+
634
+ // Should have both conditionals
635
+ expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
636
+ expect(output).toMatch(/_twColorScheme\s*===\s*['"]light['"]/);
637
+ });
638
+
639
+ it("should inject hook once for multiple elements with color scheme modifiers", () => {
640
+ const input = `
641
+ import React from 'react';
642
+ import { View, Text } from 'react-native';
643
+
644
+ export function Component() {
645
+ return (
646
+ <>
647
+ <View className="dark:bg-gray-900" />
648
+ <Text className="dark:text-white" />
649
+ <View className="light:bg-white" />
650
+ </>
651
+ );
652
+ }
653
+ `;
654
+
655
+ const output = transform(input, undefined, true);
656
+
657
+ // Count occurrences of useColorScheme() call - should be exactly 1
658
+ const hookCallMatches = output.match(/=\s*useColorScheme\(\)/g);
659
+ expect(hookCallMatches).toHaveLength(1);
660
+
661
+ // Should have color scheme variable
662
+ expect(output).toContain("_twColorScheme");
663
+ });
664
+
665
+ it("should work with color scheme and platform modifiers together", () => {
666
+ const input = `
667
+ import React from 'react';
668
+ import { View } from 'react-native';
669
+
670
+ export function Component() {
671
+ return (
672
+ <View className="p-4 ios:p-6 dark:bg-gray-900" />
673
+ );
674
+ }
675
+ `;
676
+
677
+ const output = transform(input, undefined, true);
678
+
679
+ // Should have Platform import
680
+ expect(output).toContain("Platform");
681
+
682
+ // Should have useColorScheme import
683
+ expect(output).toContain("useColorScheme");
684
+
685
+ // Should have Platform.select for ios:
686
+ expect(output).toContain("Platform.select");
687
+ expect(output).toContain("_ios_p_6");
688
+
689
+ // Should have color scheme conditional for dark:
690
+ expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
691
+ expect(output).toContain("_dark_bg_gray_900");
692
+ });
693
+
694
+ it("should only add useColorScheme import once when needed", () => {
695
+ const input = `
696
+ import React from 'react';
697
+ import { View } from 'react-native';
698
+
699
+ export function Component() {
700
+ return (
701
+ <>
702
+ <View className="dark:bg-black" />
703
+ <View className="light:bg-white" />
704
+ </>
705
+ );
706
+ }
707
+ `;
708
+
709
+ const output = transform(input, undefined, true);
710
+
711
+ // Count useColorScheme imports
712
+ const importMatches = output.match(/import.*useColorScheme.*from ['"]react-native['"]/g);
713
+ expect(importMatches).toHaveLength(1);
714
+ });
715
+
716
+ it("should merge with existing useColorScheme import", () => {
717
+ const input = `
718
+ import React from 'react';
719
+ import { View, useColorScheme } from 'react-native';
720
+
721
+ export function Component() {
722
+ return <View className="dark:bg-gray-900" />;
723
+ }
724
+ `;
725
+
726
+ const output = transform(input, undefined, true);
727
+
728
+ // Should still use useColorScheme
729
+ expect(output).toContain("useColorScheme");
730
+
731
+ // Should inject hook call
732
+ expect(output).toContain("_twColorScheme");
733
+ expect(output).toContain("useColorScheme()");
734
+ });
735
+
736
+ it("should work with concise arrow functions", () => {
737
+ const input = `
738
+ import React from 'react';
739
+ import { View } from 'react-native';
740
+
741
+ const Component = () => <View className="dark:bg-gray-900" />;
742
+ `;
743
+
744
+ const output = transform(input, undefined, true);
745
+
746
+ // Should inject useColorScheme import
747
+ expect(output).toContain("useColorScheme");
748
+
749
+ // Should convert concise arrow to block statement and inject hook
750
+ expect(output).toContain("_twColorScheme");
751
+ expect(output).toContain("useColorScheme()");
752
+ expect(output).toContain("return");
753
+
754
+ // Should have the style
755
+ expect(output).toContain("_dark_bg_gray_900");
756
+ expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
757
+ });
758
+
759
+ it("should inject hook at component level when dark: used in nested callback", () => {
760
+ const input = `
761
+ import React from 'react';
762
+ import { View } from 'react-native';
763
+
764
+ export function Component() {
765
+ const items = [1, 2, 3];
766
+ return (
767
+ <View>
768
+ {items.map(item => (
769
+ <View key={item} className="dark:bg-gray-900" />
770
+ ))}
771
+ </View>
772
+ );
773
+ }
774
+ `;
775
+
776
+ const output = transform(input, undefined, true);
777
+
778
+ // Should inject hook at Component level (not in map callback)
779
+ expect(output).toContain("_twColorScheme");
780
+ expect(output).toContain("useColorScheme()");
781
+
782
+ // Hook should be injected in Component function, not in map callback
783
+ // Count occurrences - should be exactly 1 at Component level
784
+ const hookCallMatches = output.match(/=\s*useColorScheme\(\)/g);
785
+ expect(hookCallMatches).toHaveLength(1);
786
+
787
+ // Should still generate conditional expression
788
+ expect(output).toContain("_dark_bg_gray_900");
789
+ expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
790
+ });
791
+
792
+ it("should handle dynamic expressions with dark:/light: modifiers", () => {
793
+ const input = `
794
+ import React from 'react';
795
+ import { View } from 'react-native';
796
+
797
+ export function Component({ isActive }) {
798
+ return (
799
+ <View className={\`p-4 \${isActive ? "dark:bg-blue-500" : "dark:bg-gray-900"}\`} />
800
+ );
801
+ }
802
+ `;
803
+
804
+ const output = transform(input, undefined, true);
805
+
806
+ // Should inject useColorScheme
807
+ expect(output).toContain("useColorScheme");
808
+ expect(output).toContain("_twColorScheme");
809
+
810
+ // Should have both dark styles
811
+ expect(output).toContain("_dark_bg_blue_500");
812
+ expect(output).toContain("_dark_bg_gray_900");
813
+
814
+ // Should have conditional expressions for color scheme
815
+ expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
816
+ });
817
+
818
+ it("should handle dynamic expressions with platform modifiers", () => {
819
+ const input = `
820
+ import React from 'react';
821
+ import { View } from 'react-native';
822
+
823
+ export function Component({ isLarge }) {
824
+ return (
825
+ <View className={\`p-4 \${isLarge ? "ios:p-8" : "ios:p-6"}\`} />
826
+ );
827
+ }
828
+ `;
829
+
830
+ const output = transform(input, undefined, true);
831
+
832
+ // Should inject Platform import
833
+ expect(output).toContain("Platform");
834
+
835
+ // Should have both ios styles
836
+ expect(output).toContain("_ios_p_8");
837
+ expect(output).toContain("_ios_p_6");
838
+
839
+ // Should have Platform.select
840
+ expect(output).toContain("Platform.select");
841
+ });
842
+
843
+ it("should skip color scheme modifiers when used outside component scope", () => {
844
+ // Suppress console.warn for this test
845
+ const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
846
+
847
+ const input = `
848
+ import React from 'react';
849
+ import { View } from 'react-native';
850
+
851
+ // Class component - no function component scope
852
+ class MyComponent extends React.Component {
853
+ render() {
854
+ return <View className="p-4 dark:bg-gray-900" />;
855
+ }
856
+ }
857
+ `;
858
+
859
+ const output = transform(input, undefined, true);
860
+
861
+ // Should warn about invalid context
862
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
863
+ expect.stringContaining("dark:/light: modifiers require a function component scope"),
864
+ );
865
+
866
+ // Should NOT inject useColorScheme import (no valid component scope)
867
+ expect(output).not.toContain("useColorScheme");
868
+
869
+ // Should NOT have _twColorScheme variable reference (would cause ReferenceError)
870
+ expect(output).not.toContain("_twColorScheme");
871
+
872
+ // Should NOT have dark: style conditional (skipped due to no component scope)
873
+ expect(output).not.toContain("_dark_bg_gray_900");
874
+
875
+ // Should still transform base classes (p-4)
876
+ expect(output).toContain("_p_4");
877
+
878
+ consoleWarnSpy.mockRestore();
879
+ });
880
+ });
881
+
882
+ describe("className visitor - custom color scheme hook", () => {
883
+ it("should use custom import source for color scheme hook", () => {
884
+ const input = `
885
+ import React from 'react';
886
+ import { View } from 'react-native';
887
+
888
+ export function Component() {
889
+ return <View className="dark:bg-gray-900" />;
890
+ }
891
+ `;
892
+
893
+ const output = transform(
894
+ input,
895
+ {
896
+ colorScheme: {
897
+ importFrom: "@/hooks/useColorScheme",
898
+ importName: "useColorScheme",
899
+ },
900
+ },
901
+ true,
902
+ );
903
+
904
+ // Should import from custom source
905
+ expect(output).toContain('from "@/hooks/useColorScheme"');
906
+ expect(output).not.toContain('useColorScheme } from "react-native"');
907
+
908
+ // Should inject hook call
909
+ expect(output).toContain("_twColorScheme = useColorScheme()");
910
+
911
+ // Should have conditional styling
912
+ expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
913
+ });
914
+
915
+ it("should use custom hook name", () => {
916
+ const input = `
917
+ import React from 'react';
918
+ import { View } from 'react-native';
919
+
920
+ export function Component() {
921
+ return <View className="dark:bg-gray-900" />;
922
+ }
923
+ `;
924
+
925
+ const output = transform(
926
+ input,
927
+ {
928
+ colorScheme: {
929
+ importFrom: "@react-navigation/native",
930
+ importName: "useTheme",
931
+ },
932
+ },
933
+ true,
934
+ );
935
+
936
+ // Should import useTheme from React Navigation
937
+ expect(output).toContain('from "@react-navigation/native"');
938
+ expect(output).toContain("useTheme");
939
+
940
+ // Should call useTheme hook
941
+ expect(output).toContain("_twColorScheme = useTheme()");
942
+
943
+ // Should have conditional styling
944
+ expect(output).toMatch(/_twColorScheme\s*===\s*['"]dark['"]/);
945
+ });
946
+
947
+ it("should merge custom hook with existing import from same source", () => {
948
+ const input = `
949
+ import React from 'react';
950
+ import { View, Text } from 'react-native';
951
+ import { useNavigation } from '@react-navigation/native';
952
+
953
+ export function Component() {
954
+ const navigation = useNavigation();
955
+ return (
956
+ <View className="dark:bg-gray-900">
957
+ <Text onPress={() => navigation.navigate('Home')}>Go Home</Text>
958
+ </View>
959
+ );
960
+ }
961
+ `;
962
+
963
+ const output = transform(
964
+ input,
965
+ {
966
+ colorScheme: {
967
+ importFrom: "@react-navigation/native",
968
+ importName: "useTheme",
969
+ },
970
+ },
971
+ true,
972
+ );
973
+
974
+ // Should merge with existing import (both useNavigation and useTheme in same import)
975
+ expect(output).toMatch(
976
+ /import\s+\{\s*useNavigation[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/,
977
+ );
978
+ expect(output).toContain("useNavigation()");
979
+ expect(output).toContain("useTheme()");
980
+
981
+ // Should only have one import from that source
982
+ const importCount = (output.match(/@react-navigation\/native/g) ?? []).length;
983
+ expect(importCount).toBe(1);
984
+ });
985
+
986
+ it("should not duplicate custom hook if already imported", () => {
987
+ const input = `
988
+ import React from 'react';
989
+ import { View } from 'react-native';
990
+ import { useColorScheme } from '@/hooks/useColorScheme';
991
+
992
+ export function Component() {
993
+ return <View className="dark:bg-gray-900" />;
994
+ }
995
+ `;
996
+
997
+ const output = transform(
998
+ input,
999
+ {
1000
+ colorScheme: {
1001
+ importFrom: "@/hooks/useColorScheme",
1002
+ importName: "useColorScheme",
1003
+ },
1004
+ },
1005
+ true,
1006
+ );
1007
+
1008
+ // Should not add duplicate import
1009
+ const importMatches = output.match(/import.*useColorScheme.*from ['"]@\/hooks\/useColorScheme['"]/g);
1010
+ expect(importMatches).toHaveLength(1);
1011
+
1012
+ // Should still inject hook call
1013
+ expect(output).toContain("_twColorScheme = useColorScheme()");
1014
+ });
1015
+
1016
+ it("should use react-native by default when no custom config provided", () => {
1017
+ const input = `
1018
+ import React from 'react';
1019
+ import { View } from 'react-native';
1020
+
1021
+ export function Component() {
1022
+ return <View className="dark:bg-gray-900" />;
1023
+ }
1024
+ `;
1025
+
1026
+ const output = transform(input, undefined, true);
1027
+
1028
+ // Should use default react-native import (can be single or double quotes)
1029
+ expect(output).toMatch(/useColorScheme\s*}\s*from\s+['"]react-native['"]/);
1030
+ expect(output).not.toContain("@/hooks");
1031
+ expect(output).not.toContain("@react-navigation");
1032
+
1033
+ // Should inject hook call with default name
1034
+ expect(output).toContain("_twColorScheme = useColorScheme()");
1035
+ });
1036
+
1037
+ it("should create separate import when only type-only import exists", () => {
1038
+ const input = `
1039
+ import React from 'react';
1040
+ import { View } from 'react-native';
1041
+ import type { NavigationProp } from '@react-navigation/native';
1042
+
1043
+ export function Component() {
1044
+ return <View className="dark:bg-gray-900" />;
1045
+ }
1046
+ `;
1047
+
1048
+ const output = transform(
1049
+ input,
1050
+ {
1051
+ colorScheme: {
1052
+ importFrom: "@react-navigation/native",
1053
+ importName: "useTheme",
1054
+ },
1055
+ },
1056
+ true,
1057
+ );
1058
+
1059
+ // TypeScript preset strips type-only imports, but the important thing is:
1060
+ // 1. useTheme hook is imported (not skipped thinking it was already imported)
1061
+ // 2. Hook is correctly called in the component
1062
+ expect(output).toMatch(/import\s+\{\s*useTheme\s*\}\s+from\s+['"]@react-navigation\/native['"]/);
1063
+ expect(output).toContain("_twColorScheme = useTheme()");
1064
+ });
1065
+
1066
+ it("should use aliased identifier when hook is already imported with alias", () => {
1067
+ const input = `
1068
+ import React from 'react';
1069
+ import { View, Text } from 'react-native';
1070
+ import { useTheme as navTheme } from '@react-navigation/native';
1071
+
1072
+ export function Component() {
1073
+ const theme = navTheme();
1074
+ return (
1075
+ <View className="dark:bg-gray-900">
1076
+ <Text>{theme.dark ? 'Dark' : 'Light'}</Text>
1077
+ </View>
1078
+ );
1079
+ }
1080
+ `;
1081
+
1082
+ const output = transform(
1083
+ input,
1084
+ {
1085
+ colorScheme: {
1086
+ importFrom: "@react-navigation/native",
1087
+ importName: "useTheme",
1088
+ },
1089
+ },
1090
+ true,
1091
+ );
1092
+
1093
+ // Should not add duplicate import
1094
+ const importMatches = output.match(
1095
+ /import\s+\{[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/g,
1096
+ );
1097
+ expect(importMatches).toHaveLength(1);
1098
+
1099
+ // Should still have the aliased import
1100
+ expect(output).toMatch(/useTheme\s+as\s+navTheme/);
1101
+
1102
+ // Should call the aliased name (navTheme), not the export name (useTheme)
1103
+ // Both the user's code and our injected hook should use navTheme
1104
+ expect(output).toContain("_twColorScheme = navTheme()");
1105
+ expect(output).not.toContain("_twColorScheme = useTheme()");
1106
+ });
1107
+
1108
+ it("should not treat type-only imports as having the hook", () => {
1109
+ const input = `
1110
+ import React from 'react';
1111
+ import { View } from 'react-native';
1112
+ import type { useColorScheme } from 'react-native';
1113
+
1114
+ export function Component() {
1115
+ return <View className="dark:bg-gray-900" />;
1116
+ }
1117
+ `;
1118
+
1119
+ const output = transform(input, undefined, true);
1120
+
1121
+ // Should add a VALUE import for useColorScheme (type import doesn't count)
1122
+ expect(output).toMatch(/import\s+\{[^}]*useColorScheme[^}]*\}\s+from\s+['"]react-native['"]/);
1123
+
1124
+ // Should inject the hook
1125
+ expect(output).toContain("_twColorScheme = useColorScheme()");
1126
+
1127
+ // Should have both type-only and value imports in output
1128
+ // (TypeScript preset keeps type imports for type checking)
1129
+ const colorSchemeMatches = output.match(/useColorScheme/g);
1130
+ expect(colorSchemeMatches).toBeTruthy();
1131
+ if (colorSchemeMatches) {
1132
+ expect(colorSchemeMatches.length).toBeGreaterThanOrEqual(2); // At least in import and hook call
1133
+ }
1134
+ });
1135
+
1136
+ it("should handle both type-only and aliased imports together", () => {
1137
+ const input = `
1138
+ import React from 'react';
1139
+ import { View, Text } from 'react-native';
1140
+ import type { Theme } from '@react-navigation/native';
1141
+ import { useTheme as getNavTheme } from '@react-navigation/native';
1142
+
1143
+ export function Component() {
1144
+ const theme = getNavTheme();
1145
+ return (
1146
+ <View className="dark:bg-gray-900">
1147
+ <Text>{theme.dark ? 'Dark Mode' : 'Light Mode'}</Text>
1148
+ </View>
1149
+ );
1150
+ }
1151
+ `;
1152
+
1153
+ const output = transform(
1154
+ input,
1155
+ {
1156
+ colorScheme: {
1157
+ importFrom: "@react-navigation/native",
1158
+ importName: "useTheme",
1159
+ },
1160
+ },
1161
+ true,
1162
+ );
1163
+
1164
+ // TypeScript preset strips type-only imports
1165
+ // The important thing is: should not add duplicate import, and should use aliased name
1166
+ expect(output).toMatch(
1167
+ /import\s+\{[^}]*useTheme\s+as\s+getNavTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/,
1168
+ );
1169
+
1170
+ // Should not add duplicate import - useTheme should only appear in the aliased import
1171
+ const useThemeImports = output.match(
1172
+ /import\s+\{[^}]*useTheme[^}]*\}\s+from\s+['"]@react-navigation\/native['"]/g,
1173
+ );
1174
+ expect(useThemeImports).toHaveLength(1);
1175
+
1176
+ // Should call the aliased name for both user code and our injected hook
1177
+ expect(output).toContain("_twColorScheme = getNavTheme()");
1178
+ expect(output).not.toContain("_twColorScheme = useTheme()");
1179
+ });
1180
+ });
1181
+
1182
+ describe("className visitor - directional border colors", () => {
1183
+ it("should transform directional border colors with preset values", () => {
1184
+ const input = `
1185
+ import { View } from 'react-native';
1186
+ export function Component() {
1187
+ return <View className="border-t-red-500 border-l-blue-500" />;
1188
+ }
1189
+ `;
1190
+
1191
+ const output = transform(input, undefined, true);
1192
+
1193
+ // Should have StyleSheet
1194
+ expect(output).toContain("StyleSheet.create");
1195
+
1196
+ // Should generate styles with borderTopColor and borderLeftColor
1197
+ expect(output).toMatch(/borderTopColor[:\s]*['"]#[0-9A-F]{6}['"]/i);
1198
+ expect(output).toMatch(/borderLeftColor[:\s]*['"]#[0-9A-F]{6}['"]/i);
1199
+
1200
+ // Should not have className in output
1201
+ expect(output).not.toContain("className");
1202
+ });
1203
+
1204
+ it("should combine directional border width and color", () => {
1205
+ const input = `
1206
+ import { View } from 'react-native';
1207
+ export function Component() {
1208
+ return <View className="border-l-2 border-l-red-500" />;
1209
+ }
1210
+ `;
1211
+
1212
+ const output = transform(input, undefined, true);
1213
+
1214
+ // Should have both borderLeftWidth and borderLeftColor in the StyleSheet
1215
+ expect(output).toMatch(/borderLeftWidth[:\s]*2/);
1216
+ expect(output).toMatch(/borderLeftColor[:\s]*['"]#[0-9A-F]{6}['"]/i);
1217
+
1218
+ // Should not have className in output
1219
+ expect(output).not.toContain("className");
1220
+ });
1221
+
1222
+ it("should support directional border colors with opacity", () => {
1223
+ const input = `
1224
+ import { View } from 'react-native';
1225
+ export function Component() {
1226
+ return <View className="border-t-red-500/50 border-b-blue-500/80" />;
1227
+ }
1228
+ `;
1229
+
1230
+ const output = transform(input, undefined, true);
1231
+
1232
+ // Should have 8-digit hex colors with alpha channel
1233
+ expect(output).toMatch(/borderTopColor[:\s]*['"]#[0-9A-F]{8}['"]/i);
1234
+ expect(output).toMatch(/borderBottomColor[:\s]*['"]#[0-9A-F]{8}['"]/i);
1235
+ });
1236
+
1237
+ it("should support directional border colors with arbitrary hex values", () => {
1238
+ const input = `
1239
+ import { View } from 'react-native';
1240
+ export function Component() {
1241
+ return <View className="border-t-[#ff0000] border-r-[#abc]" />;
1242
+ }
1243
+ `;
1244
+
1245
+ const output = transform(input, undefined, true);
1246
+
1247
+ // Should have borderTopColor and borderRightColor
1248
+ expect(output).toMatch(/borderTopColor[:\s]*['"]#[0-9a-fA-F]{6}['"]/);
1249
+ expect(output).toMatch(/borderRightColor[:\s]*['"]#[0-9a-fA-F]{6}['"]/);
1250
+ });
1251
+
1252
+ it("should support all four directional border colors", () => {
1253
+ const input = `
1254
+ import { View } from 'react-native';
1255
+ export function Component() {
1256
+ return (
1257
+ <View className="border-t-red-500 border-r-blue-500 border-b-green-500 border-l-yellow-500" />
1258
+ );
1259
+ }
1260
+ `;
1261
+
1262
+ const output = transform(input, undefined, true);
1263
+
1264
+ // Should have all four directional color properties
1265
+ expect(output).toMatch(/borderTopColor[:\s]*['"]#[0-9A-F]{6}['"]/i);
1266
+ expect(output).toMatch(/borderRightColor[:\s]*['"]#[0-9A-F]{6}['"]/i);
1267
+ expect(output).toMatch(/borderBottomColor[:\s]*['"]#[0-9A-F]{6}['"]/i);
1268
+ expect(output).toMatch(/borderLeftColor[:\s]*['"]#[0-9A-F]{6}['"]/i);
1269
+ });
1270
+
1271
+ it("should combine directional widths, colors, and general border color", () => {
1272
+ const input = `
1273
+ import { View } from 'react-native';
1274
+ export function Component() {
1275
+ return (
1276
+ <View className="border border-gray-300 border-l-4 border-l-blue-500" />
1277
+ );
1278
+ }
1279
+ `;
1280
+
1281
+ const output = transform(input, undefined, true);
1282
+
1283
+ // Should have general border properties
1284
+ expect(output).toMatch(/borderWidth[:\s]*1/);
1285
+ expect(output).toMatch(/borderColor[:\s]*['"]#[0-9A-F]{6}['"]/i);
1286
+
1287
+ // Should have directional left border properties
1288
+ expect(output).toMatch(/borderLeftWidth[:\s]*4/);
1289
+ expect(output).toMatch(/borderLeftColor[:\s]*['"]#[0-9A-F]{6}['"]/i);
1290
+ });
1291
+
1292
+ it("should work with dynamic className containing directional border colors", () => {
1293
+ const input = `
1294
+ import { View } from 'react-native';
1295
+ export function Component({ isError }) {
1296
+ return (
1297
+ <View className={\`border-t-2 \${isError ? 'border-t-red-500' : 'border-t-gray-300'}\`} />
1298
+ );
1299
+ }
1300
+ `;
1301
+
1302
+ const output = transform(input, undefined, true);
1303
+
1304
+ // Should have StyleSheet with both color options
1305
+ expect(output).toContain("_border_t_2");
1306
+ expect(output).toContain("_border_t_red_500");
1307
+ expect(output).toContain("_border_t_gray_300");
1308
+
1309
+ // Should have conditional expression with both styles
1310
+ expect(output).toMatch(/isError\s*\?\s*_twStyles\._border_t_red_500/);
1311
+ });
1312
+ });