@mgcrea/react-native-tailwind 0.13.0 → 0.15.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 (73) hide show
  1. package/README.md +33 -30
  2. package/dist/babel/config-loader.d.ts +10 -0
  3. package/dist/babel/config-loader.test.ts +75 -21
  4. package/dist/babel/config-loader.ts +100 -2
  5. package/dist/babel/index.cjs +439 -46
  6. package/dist/babel/plugin/state.d.ts +4 -0
  7. package/dist/babel/plugin/state.ts +8 -0
  8. package/dist/babel/plugin/visitors/className.test.ts +313 -0
  9. package/dist/babel/plugin/visitors/className.ts +36 -8
  10. package/dist/babel/plugin/visitors/imports.ts +16 -1
  11. package/dist/babel/plugin/visitors/program.ts +19 -2
  12. package/dist/babel/plugin/visitors/tw.test.ts +151 -0
  13. package/dist/babel/utils/directionalModifierProcessing.d.ts +34 -0
  14. package/dist/babel/utils/directionalModifierProcessing.ts +99 -0
  15. package/dist/babel/utils/styleInjection.d.ts +16 -0
  16. package/dist/babel/utils/styleInjection.ts +138 -7
  17. package/dist/babel/utils/twProcessing.d.ts +2 -0
  18. package/dist/babel/utils/twProcessing.ts +92 -3
  19. package/dist/parser/borders.js +1 -1
  20. package/dist/parser/borders.test.js +1 -1
  21. package/dist/parser/index.d.ts +3 -2
  22. package/dist/parser/index.js +1 -1
  23. package/dist/parser/layout.d.ts +3 -1
  24. package/dist/parser/layout.js +1 -1
  25. package/dist/parser/layout.test.js +1 -1
  26. package/dist/parser/modifiers.d.ts +32 -2
  27. package/dist/parser/modifiers.js +1 -1
  28. package/dist/parser/modifiers.test.js +1 -1
  29. package/dist/parser/sizing.d.ts +3 -1
  30. package/dist/parser/sizing.js +1 -1
  31. package/dist/parser/sizing.test.js +1 -1
  32. package/dist/parser/spacing.d.ts +4 -2
  33. package/dist/parser/spacing.js +1 -1
  34. package/dist/parser/spacing.test.js +1 -1
  35. package/dist/parser/transforms.d.ts +3 -1
  36. package/dist/parser/transforms.js +1 -1
  37. package/dist/parser/transforms.test.js +1 -1
  38. package/dist/parser/typography.test.js +1 -1
  39. package/dist/runtime.cjs +1 -1
  40. package/dist/runtime.cjs.map +3 -3
  41. package/dist/runtime.d.ts +2 -0
  42. package/dist/runtime.js +1 -1
  43. package/dist/runtime.js.map +3 -3
  44. package/dist/runtime.test.js +1 -1
  45. package/package.json +6 -6
  46. package/src/babel/config-loader.test.ts +75 -21
  47. package/src/babel/config-loader.ts +100 -2
  48. package/src/babel/plugin/state.ts +8 -0
  49. package/src/babel/plugin/visitors/className.test.ts +313 -0
  50. package/src/babel/plugin/visitors/className.ts +36 -8
  51. package/src/babel/plugin/visitors/imports.ts +16 -1
  52. package/src/babel/plugin/visitors/program.ts +19 -2
  53. package/src/babel/plugin/visitors/tw.test.ts +151 -0
  54. package/src/babel/utils/directionalModifierProcessing.ts +99 -0
  55. package/src/babel/utils/styleInjection.ts +138 -7
  56. package/src/babel/utils/twProcessing.ts +92 -3
  57. package/src/parser/borders.test.ts +104 -0
  58. package/src/parser/borders.ts +50 -7
  59. package/src/parser/index.ts +8 -5
  60. package/src/parser/layout.test.ts +168 -0
  61. package/src/parser/layout.ts +107 -8
  62. package/src/parser/modifiers.test.ts +206 -0
  63. package/src/parser/modifiers.ts +62 -3
  64. package/src/parser/sizing.test.ts +56 -0
  65. package/src/parser/sizing.ts +20 -15
  66. package/src/parser/spacing.test.ts +123 -0
  67. package/src/parser/spacing.ts +30 -15
  68. package/src/parser/transforms.test.ts +57 -0
  69. package/src/parser/transforms.ts +7 -3
  70. package/src/parser/typography.test.ts +8 -0
  71. package/src/parser/typography.ts +4 -0
  72. package/src/runtime.test.ts +149 -0
  73. package/src/runtime.ts +53 -1
@@ -618,3 +618,154 @@ describe("tw/twStyle - integration with className", () => {
618
618
  expect(output).toContain("_m_4_p_2");
619
619
  });
620
620
  });
621
+
622
+ describe("tw visitor - directional modifiers (RTL/LTR)", () => {
623
+ it("should transform rtl: modifier in tw template", () => {
624
+ const input = `
625
+ import { tw } from '@mgcrea/react-native-tailwind';
626
+
627
+ function MyComponent() {
628
+ const styles = tw\`p-4 rtl:mr-4\`;
629
+ return null;
630
+ }
631
+ `;
632
+
633
+ const output = transform(input);
634
+
635
+ // Should import I18nManager
636
+ expect(output).toContain("I18nManager");
637
+
638
+ // Should declare _twIsRTL variable
639
+ expect(output).toContain("_twIsRTL");
640
+ expect(output).toContain("I18nManager.isRTL");
641
+
642
+ // Should have style array with conditional
643
+ expect(output).toContain("style:");
644
+ expect(output).toContain("_twStyles._p_4");
645
+ expect(output).toMatch(/_twIsRTL\s*&&\s*_twStyles\._rtl_mr_4/);
646
+
647
+ // Should have rtlStyle property
648
+ expect(output).toContain("rtlStyle:");
649
+ expect(output).toContain("_twStyles._rtl_mr_4");
650
+ });
651
+
652
+ it("should transform ltr: modifier with negated conditional", () => {
653
+ const input = `
654
+ import { tw } from '@mgcrea/react-native-tailwind';
655
+
656
+ function MyComponent() {
657
+ const styles = tw\`p-4 ltr:ml-4\`;
658
+ return null;
659
+ }
660
+ `;
661
+
662
+ const output = transform(input);
663
+
664
+ // Should import I18nManager
665
+ expect(output).toContain("I18nManager");
666
+
667
+ // Should have negated conditional for LTR (!_twIsRTL)
668
+ expect(output).toMatch(/!\s*_twIsRTL\s*&&\s*_twStyles\._ltr_ml_4/);
669
+
670
+ // Should have ltrStyle property
671
+ expect(output).toContain("ltrStyle:");
672
+ });
673
+
674
+ it("should combine rtl: and ltr: modifiers", () => {
675
+ const input = `
676
+ import { tw } from '@mgcrea/react-native-tailwind';
677
+
678
+ function MyComponent() {
679
+ const styles = tw\`rtl:mr-4 ltr:ml-4\`;
680
+ return null;
681
+ }
682
+ `;
683
+
684
+ const output = transform(input);
685
+
686
+ // Should have both conditionals
687
+ expect(output).toMatch(/_twIsRTL\s*&&\s*_twStyles\._rtl_mr_4/);
688
+ expect(output).toMatch(/!\s*_twIsRTL\s*&&\s*_twStyles\._ltr_ml_4/);
689
+
690
+ // Should have both style properties
691
+ expect(output).toContain("rtlStyle:");
692
+ expect(output).toContain("ltrStyle:");
693
+ });
694
+
695
+ it("should combine directional modifiers with platform modifiers", () => {
696
+ const input = `
697
+ import { tw } from '@mgcrea/react-native-tailwind';
698
+
699
+ function MyComponent() {
700
+ const styles = tw\`p-4 ios:p-6 rtl:mr-4\`;
701
+ return null;
702
+ }
703
+ `;
704
+
705
+ const output = transform(input);
706
+
707
+ // Should have Platform import
708
+ expect(output).toContain("Platform");
709
+
710
+ // Should have I18nManager import
711
+ expect(output).toContain("I18nManager");
712
+
713
+ // Should have both modifiers in style array
714
+ expect(output).toContain("Platform.select");
715
+ expect(output).toMatch(/_twIsRTL\s*&&/);
716
+
717
+ // Should have iosStyle and rtlStyle properties
718
+ expect(output).toContain("iosStyle:");
719
+ expect(output).toContain("rtlStyle:");
720
+ });
721
+
722
+ it("should combine directional modifiers with state modifiers", () => {
723
+ const input = `
724
+ import { tw } from '@mgcrea/react-native-tailwind';
725
+
726
+ function MyComponent() {
727
+ const styles = tw\`bg-white active:bg-blue-500 rtl:pr-4\`;
728
+ return null;
729
+ }
730
+ `;
731
+
732
+ const output = transform(input);
733
+
734
+ // Should have I18nManager import
735
+ expect(output).toContain("I18nManager");
736
+
737
+ // Should have directional conditional
738
+ expect(output).toMatch(/_twIsRTL\s*&&/);
739
+
740
+ // Should have activeStyle property
741
+ expect(output).toContain("activeStyle:");
742
+ expect(output).toContain("_twStyles._active_bg_blue_500");
743
+
744
+ // Should have rtlStyle property
745
+ expect(output).toContain("rtlStyle:");
746
+ });
747
+
748
+ it("should work with twStyle function for RTL modifiers", () => {
749
+ const input = `
750
+ import { twStyle } from '@mgcrea/react-native-tailwind';
751
+
752
+ function MyComponent() {
753
+ const styles = twStyle('p-4 rtl:mr-4 ltr:ml-4');
754
+ return null;
755
+ }
756
+ `;
757
+
758
+ const output = transform(input);
759
+
760
+ // Should import I18nManager
761
+ expect(output).toContain("I18nManager");
762
+
763
+ // Should have both conditionals
764
+ expect(output).toMatch(/_twIsRTL\s*&&/);
765
+ expect(output).toMatch(/!\s*_twIsRTL\s*&&/);
766
+
767
+ // Should have both style properties
768
+ expect(output).toContain("rtlStyle:");
769
+ expect(output).toContain("ltrStyle:");
770
+ });
771
+ });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Utility functions for processing directional modifiers (rtl:, ltr:)
3
+ */
4
+ import type * as BabelTypes from "@babel/types";
5
+ import type { CustomTheme, ParsedModifier } from "../../parser/index.js";
6
+ import type { StyleObject } from "../../types/core.js";
7
+ /**
8
+ * Plugin state interface (subset needed for directional modifier processing)
9
+ */
10
+ export interface DirectionalModifierProcessingState {
11
+ styleRegistry: Map<string, StyleObject>;
12
+ customTheme: CustomTheme;
13
+ stylesIdentifier: string;
14
+ needsI18nManagerImport: boolean;
15
+ i18nManagerVariableName: string;
16
+ }
17
+ /**
18
+ * Process directional modifiers and generate conditional style expressions
19
+ *
20
+ * @param directionalModifiers - Array of parsed directional modifiers
21
+ * @param state - Plugin state
22
+ * @param parseClassName - Function to parse class names into style objects
23
+ * @param generateStyleKey - Function to generate unique style keys
24
+ * @param t - Babel types
25
+ * @returns Array of AST nodes for conditional expressions
26
+ *
27
+ * @example
28
+ * Input: [{ modifier: "rtl", baseClass: "mr-4" }, { modifier: "ltr", baseClass: "ml-4" }]
29
+ * Output: [
30
+ * _twIsRTL && styles._rtl_mr_4,
31
+ * !_twIsRTL && styles._ltr_ml_4
32
+ * ]
33
+ */
34
+ export declare function processDirectionalModifiers(directionalModifiers: ParsedModifier[], state: DirectionalModifierProcessingState, parseClassName: (className: string, customTheme?: CustomTheme) => StyleObject, generateStyleKey: (className: string) => string, t: typeof BabelTypes): BabelTypes.Expression[];
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Utility functions for processing directional modifiers (rtl:, ltr:)
3
+ */
4
+
5
+ import type * as BabelTypes from "@babel/types";
6
+ import type { CustomTheme, DirectionalModifierType, ParsedModifier } from "../../parser/index.js";
7
+ import type { StyleObject } from "../../types/core.js";
8
+ import { hasRuntimeDimensions } from "./windowDimensionsProcessing.js";
9
+
10
+ /**
11
+ * Plugin state interface (subset needed for directional modifier processing)
12
+ */
13
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
14
+ export interface DirectionalModifierProcessingState {
15
+ styleRegistry: Map<string, StyleObject>;
16
+ customTheme: CustomTheme;
17
+ stylesIdentifier: string;
18
+ needsI18nManagerImport: boolean;
19
+ i18nManagerVariableName: string;
20
+ }
21
+
22
+ /**
23
+ * Process directional modifiers and generate conditional style expressions
24
+ *
25
+ * @param directionalModifiers - Array of parsed directional modifiers
26
+ * @param state - Plugin state
27
+ * @param parseClassName - Function to parse class names into style objects
28
+ * @param generateStyleKey - Function to generate unique style keys
29
+ * @param t - Babel types
30
+ * @returns Array of AST nodes for conditional expressions
31
+ *
32
+ * @example
33
+ * Input: [{ modifier: "rtl", baseClass: "mr-4" }, { modifier: "ltr", baseClass: "ml-4" }]
34
+ * Output: [
35
+ * _twIsRTL && styles._rtl_mr_4,
36
+ * !_twIsRTL && styles._ltr_ml_4
37
+ * ]
38
+ */
39
+ export function processDirectionalModifiers(
40
+ directionalModifiers: ParsedModifier[],
41
+ state: DirectionalModifierProcessingState,
42
+ parseClassName: (className: string, customTheme?: CustomTheme) => StyleObject,
43
+ generateStyleKey: (className: string) => string,
44
+ t: typeof BabelTypes,
45
+ ): BabelTypes.Expression[] {
46
+ // Mark that we need I18nManager import
47
+ state.needsI18nManagerImport = true;
48
+
49
+ // Group modifiers by direction (rtl, ltr)
50
+ const modifiersByDirection = new Map<DirectionalModifierType, ParsedModifier[]>();
51
+
52
+ for (const mod of directionalModifiers) {
53
+ const direction = mod.modifier as DirectionalModifierType;
54
+ if (!modifiersByDirection.has(direction)) {
55
+ modifiersByDirection.set(direction, []);
56
+ }
57
+ const directionGroup = modifiersByDirection.get(direction);
58
+ if (directionGroup) {
59
+ directionGroup.push(mod);
60
+ }
61
+ }
62
+
63
+ // Build conditional expressions for each direction
64
+ const conditionalExpressions: BabelTypes.Expression[] = [];
65
+
66
+ for (const [direction, modifiers] of modifiersByDirection) {
67
+ // Parse all classes for this direction together
68
+ const classNames = modifiers.map((m) => m.baseClass).join(" ");
69
+ const styleObject = parseClassName(classNames, state.customTheme);
70
+
71
+ // Check for runtime dimensions (w-screen, h-screen)
72
+ if (hasRuntimeDimensions(styleObject)) {
73
+ throw new Error(
74
+ `w-screen and h-screen cannot be combined with directional modifiers (rtl:, ltr:). ` +
75
+ `Found in: "${direction}:${classNames}". ` +
76
+ `Use w-screen/h-screen without modifiers instead.`,
77
+ );
78
+ }
79
+
80
+ const styleKey = generateStyleKey(`${direction}_${classNames}`);
81
+
82
+ // Register style in the registry
83
+ state.styleRegistry.set(styleKey, styleObject);
84
+
85
+ // Create conditional:
86
+ // - For rtl: _twIsRTL && styles._rtl_...
87
+ // - For ltr: !_twIsRTL && styles._ltr_...
88
+ const rtlVariable = t.identifier(state.i18nManagerVariableName);
89
+ const directionCheck = direction === "rtl" ? rtlVariable : t.unaryExpression("!", rtlVariable);
90
+
91
+ const styleReference = t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(styleKey));
92
+
93
+ const conditionalExpression = t.logicalExpression("&&", directionCheck, styleReference);
94
+
95
+ conditionalExpressions.push(conditionalExpression);
96
+ }
97
+
98
+ return conditionalExpressions;
99
+ }
@@ -27,6 +27,22 @@ export declare function addColorSchemeImport(path: NodePath<BabelTypes.Program>,
27
27
  * @returns true if hook was injected, false if already exists
28
28
  */
29
29
  export declare function injectColorSchemeHook(functionPath: NodePath<BabelTypes.Function>, colorSchemeVariableName: string, hookName: string, localIdentifier: string | undefined, t: typeof BabelTypes): boolean;
30
+ /**
31
+ * Add I18nManager import to the file or merge with existing react-native import
32
+ */
33
+ export declare function addI18nManagerImport(path: NodePath<BabelTypes.Program>, t: typeof BabelTypes): void;
34
+ /**
35
+ * Inject I18nManager.isRTL variable at the top of the file (after imports and directives)
36
+ *
37
+ * Unlike hooks (useColorScheme, useWindowDimensions), I18nManager.isRTL is not a hook
38
+ * and can be accessed at module level. This is injected once per file.
39
+ *
40
+ * @param path - Program path
41
+ * @param variableName - Name for the RTL variable (e.g., '_twIsRTL')
42
+ * @param localIdentifier - Local identifier if I18nManager is already imported with an alias
43
+ * @param t - Babel types
44
+ */
45
+ export declare function injectI18nManagerVariable(path: NodePath<BabelTypes.Program>, variableName: string, localIdentifier: string | undefined, t: typeof BabelTypes): void;
30
46
  /**
31
47
  * Add useWindowDimensions import to the file or merge with existing react-native import
32
48
  */
@@ -220,6 +220,128 @@ export function injectColorSchemeHook(
220
220
  return true;
221
221
  }
222
222
 
223
+ /**
224
+ * Add I18nManager import to the file or merge with existing react-native import
225
+ */
226
+ export function addI18nManagerImport(path: NodePath<BabelTypes.Program>, t: typeof BabelTypes): void {
227
+ // Check if there's already a value import from react-native
228
+ const body = path.node.body;
229
+ let existingValueImport: BabelTypes.ImportDeclaration | null = null;
230
+
231
+ for (const statement of body) {
232
+ if (t.isImportDeclaration(statement) && statement.source.value === "react-native") {
233
+ // Skip type-only imports (they get erased at runtime)
234
+ if (statement.importKind === "type") {
235
+ continue;
236
+ }
237
+ // Skip namespace imports (import * as RN) - can't add named specifiers to them
238
+ const hasNamespaceImport = statement.specifiers.some((spec) => t.isImportNamespaceSpecifier(spec));
239
+ if (hasNamespaceImport) {
240
+ continue;
241
+ }
242
+ existingValueImport = statement;
243
+ break; // Found a value import, we can stop
244
+ }
245
+ }
246
+
247
+ if (existingValueImport) {
248
+ // Check if I18nManager is already imported
249
+ const hasI18nManager = existingValueImport.specifiers.some(
250
+ (spec) =>
251
+ t.isImportSpecifier(spec) &&
252
+ spec.imported.type === "Identifier" &&
253
+ spec.imported.name === "I18nManager",
254
+ );
255
+
256
+ if (!hasI18nManager) {
257
+ // Add I18nManager to existing value import
258
+ existingValueImport.specifiers.push(
259
+ t.importSpecifier(t.identifier("I18nManager"), t.identifier("I18nManager")),
260
+ );
261
+ }
262
+ } else {
263
+ // No value import exists - create a new one
264
+ // (Don't merge with type-only or namespace imports)
265
+ const importDeclaration = t.importDeclaration(
266
+ [t.importSpecifier(t.identifier("I18nManager"), t.identifier("I18nManager"))],
267
+ t.stringLiteral("react-native"),
268
+ );
269
+ path.unshiftContainer("body", importDeclaration);
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Inject I18nManager.isRTL variable at the top of the file (after imports and directives)
275
+ *
276
+ * Unlike hooks (useColorScheme, useWindowDimensions), I18nManager.isRTL is not a hook
277
+ * and can be accessed at module level. This is injected once per file.
278
+ *
279
+ * @param path - Program path
280
+ * @param variableName - Name for the RTL variable (e.g., '_twIsRTL')
281
+ * @param localIdentifier - Local identifier if I18nManager is already imported with an alias
282
+ * @param t - Babel types
283
+ */
284
+ export function injectI18nManagerVariable(
285
+ path: NodePath<BabelTypes.Program>,
286
+ variableName: string,
287
+ localIdentifier: string | undefined,
288
+ t: typeof BabelTypes,
289
+ ): void {
290
+ const body = path.node.body;
291
+
292
+ // Check if variable is already declared
293
+ for (const statement of body) {
294
+ if (
295
+ t.isVariableDeclaration(statement) &&
296
+ statement.declarations.length > 0 &&
297
+ t.isVariableDeclarator(statement.declarations[0])
298
+ ) {
299
+ const declarator = statement.declarations[0];
300
+ if (t.isIdentifier(declarator.id) && declarator.id.name === variableName) {
301
+ return; // Already injected
302
+ }
303
+ }
304
+ }
305
+
306
+ // Use the local identifier if I18nManager was already imported with an alias,
307
+ // otherwise use 'I18nManager'
308
+ // e.g., import { I18nManager as RTL } → use RTL.isRTL
309
+ const identifierToUse = localIdentifier ?? "I18nManager";
310
+
311
+ // Create: const _twIsRTL = I18nManager.isRTL; (or aliased name if already imported)
312
+ const i18nVariable = t.variableDeclaration("const", [
313
+ t.variableDeclarator(
314
+ t.identifier(variableName),
315
+ t.memberExpression(t.identifier(identifierToUse), t.identifier("isRTL")),
316
+ ),
317
+ ]);
318
+
319
+ // Find the index to insert after all imports and directives ('use client', 'use strict', etc.)
320
+ let insertIndex = 0;
321
+
322
+ for (let i = 0; i < body.length; i++) {
323
+ const statement = body[i];
324
+
325
+ // Skip directives ('use client', 'use strict', etc.)
326
+ if (t.isExpressionStatement(statement) && t.isStringLiteral(statement.expression)) {
327
+ insertIndex = i + 1;
328
+ continue;
329
+ }
330
+
331
+ // Skip imports
332
+ if (t.isImportDeclaration(statement)) {
333
+ insertIndex = i + 1;
334
+ continue;
335
+ }
336
+
337
+ // Stop at the first non-directive, non-import statement
338
+ break;
339
+ }
340
+
341
+ // Insert after imports and directives
342
+ body.splice(insertIndex, 0, i18nVariable);
343
+ }
344
+
223
345
  /**
224
346
  * Add useWindowDimensions import to the file or merge with existing react-native import
225
347
  */
@@ -377,20 +499,29 @@ export function injectStylesAtTop(
377
499
  ),
378
500
  ]);
379
501
 
380
- // Find the index to insert after all imports
502
+ // Find the index to insert after all imports and directives ('use client', 'use strict', etc.)
381
503
  const body = path.node.body;
382
504
  let insertIndex = 0;
383
505
 
384
- // Find the last import statement
385
506
  for (let i = 0; i < body.length; i++) {
386
- if (t.isImportDeclaration(body[i])) {
507
+ const statement = body[i];
508
+
509
+ // Skip directives ('use client', 'use strict', etc.)
510
+ if (t.isExpressionStatement(statement) && t.isStringLiteral(statement.expression)) {
387
511
  insertIndex = i + 1;
388
- } else {
389
- // Stop at the first non-import statement
390
- break;
512
+ continue;
391
513
  }
514
+
515
+ // Skip imports
516
+ if (t.isImportDeclaration(statement)) {
517
+ insertIndex = i + 1;
518
+ continue;
519
+ }
520
+
521
+ // Stop at the first non-directive, non-import statement
522
+ break;
392
523
  }
393
524
 
394
- // Insert StyleSheet.create after imports
525
+ // Insert StyleSheet.create after imports and directives
395
526
  body.splice(insertIndex, 0, styleSheet);
396
527
  }
@@ -19,6 +19,8 @@ export interface TwProcessingState {
19
19
  functionComponentsNeedingColorScheme: Set<NodePath<BabelTypes.Function>>;
20
20
  colorSchemeLocalIdentifier?: string;
21
21
  needsPlatformImport: boolean;
22
+ needsI18nManagerImport: boolean;
23
+ i18nManagerVariableName: string;
22
24
  }
23
25
  /**
24
26
  * Process tw`...` or twStyle('...') call and replace with TwStyle object
@@ -8,12 +8,14 @@ import type { CustomTheme, ModifierType, ParsedModifier } from "../../parser/ind
8
8
  import {
9
9
  expandSchemeModifier,
10
10
  isColorSchemeModifier,
11
+ isDirectionalModifier,
11
12
  isPlatformModifier,
12
13
  isSchemeModifier,
13
14
  } from "../../parser/index.js";
14
15
  import type { SchemeModifierConfig } from "../../types/config.js";
15
16
  import type { StyleObject } from "../../types/core.js";
16
17
  import { processColorSchemeModifiers } from "./colorSchemeModifierProcessing.js";
18
+ import { processDirectionalModifiers } from "./directionalModifierProcessing.js";
17
19
  import { processPlatformModifiers } from "./platformModifierProcessing.js";
18
20
  import { hasRuntimeDimensions } from "./windowDimensionsProcessing.js";
19
21
 
@@ -33,6 +35,9 @@ export interface TwProcessingState {
33
35
  colorSchemeLocalIdentifier?: string;
34
36
  // Platform support (for ios:/android:/web: modifiers)
35
37
  needsPlatformImport: boolean;
38
+ // Directional support (for rtl:/ltr: modifiers)
39
+ needsI18nManagerImport: boolean;
40
+ i18nManagerVariableName: string;
36
41
  }
37
42
 
38
43
  /**
@@ -102,11 +107,15 @@ export function processTwCall(
102
107
  objectProperties.push(t.objectProperty(t.identifier("style"), t.objectExpression([])));
103
108
  }
104
109
 
105
- // Separate color-scheme and platform modifiers from other modifiers
110
+ // Separate color-scheme, platform, and directional modifiers from other modifiers
106
111
  const colorSchemeModifiers = modifierClasses.filter((m) => isColorSchemeModifier(m.modifier));
107
112
  const platformModifiers = modifierClasses.filter((m) => isPlatformModifier(m.modifier));
113
+ const directionalModifiers = modifierClasses.filter((m) => isDirectionalModifier(m.modifier));
108
114
  const otherModifiers = modifierClasses.filter(
109
- (m) => !isColorSchemeModifier(m.modifier) && !isPlatformModifier(m.modifier),
115
+ (m) =>
116
+ !isColorSchemeModifier(m.modifier) &&
117
+ !isPlatformModifier(m.modifier) &&
118
+ !isDirectionalModifier(m.modifier),
110
119
  );
111
120
 
112
121
  // Check if we need color scheme support
@@ -293,7 +302,87 @@ export function processTwCall(
293
302
  }
294
303
  }
295
304
 
296
- // Group other modifiers by type (non-color-scheme and non-platform modifiers)
305
+ // Process directional modifiers if present
306
+ const hasDirectionalModifiers = directionalModifiers.length > 0;
307
+
308
+ if (hasDirectionalModifiers) {
309
+ // Mark that we need I18nManager import
310
+ state.needsI18nManagerImport = true;
311
+
312
+ // Generate directional conditional expressions
313
+ const directionalConditionals = processDirectionalModifiers(
314
+ directionalModifiers,
315
+ state,
316
+ parseClassName,
317
+ generateStyleKey,
318
+ t,
319
+ );
320
+
321
+ // If we already have a style array (from color scheme or platform modifiers), add to it
322
+ // Otherwise, convert style property to an array
323
+ const styleProperty = objectProperties.find(
324
+ (prop) => t.isIdentifier(prop.key) && prop.key.name === "style",
325
+ );
326
+
327
+ if (styleProperty && t.isArrayExpression(styleProperty.value)) {
328
+ // Already have style array, add directional conditionals to it
329
+ styleProperty.value.elements.push(...directionalConditionals);
330
+ } else {
331
+ // No existing array, create style array with base + directional conditionals
332
+ const styleArrayElements: BabelTypes.Expression[] = [];
333
+
334
+ // Add base style if present
335
+ if (baseClasses.length > 0) {
336
+ const baseClassName = baseClasses.join(" ");
337
+ const baseStyleObject = parseClassName(baseClassName, state.customTheme);
338
+ const baseStyleKey = generateStyleKey(baseClassName);
339
+ state.styleRegistry.set(baseStyleKey, baseStyleObject);
340
+ styleArrayElements.push(
341
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey)),
342
+ );
343
+ }
344
+
345
+ // Add directional conditionals
346
+ styleArrayElements.push(...directionalConditionals);
347
+
348
+ // Replace style property with array
349
+ objectProperties[0] = t.objectProperty(t.identifier("style"), t.arrayExpression(styleArrayElements));
350
+ }
351
+
352
+ // Also add rtlStyle/ltrStyle properties for manual processing
353
+ const rtlModifiers = directionalModifiers.filter((m) => m.modifier === "rtl");
354
+ const ltrModifiers = directionalModifiers.filter((m) => m.modifier === "ltr");
355
+
356
+ if (rtlModifiers.length > 0) {
357
+ const rtlClassNames = rtlModifiers.map((m) => m.baseClass).join(" ");
358
+ const rtlStyleObject = parseClassName(rtlClassNames, state.customTheme);
359
+ const rtlStyleKey = generateStyleKey(`rtl_${rtlClassNames}`);
360
+ state.styleRegistry.set(rtlStyleKey, rtlStyleObject);
361
+
362
+ objectProperties.push(
363
+ t.objectProperty(
364
+ t.identifier("rtlStyle"),
365
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(rtlStyleKey)),
366
+ ),
367
+ );
368
+ }
369
+
370
+ if (ltrModifiers.length > 0) {
371
+ const ltrClassNames = ltrModifiers.map((m) => m.baseClass).join(" ");
372
+ const ltrStyleObject = parseClassName(ltrClassNames, state.customTheme);
373
+ const ltrStyleKey = generateStyleKey(`ltr_${ltrClassNames}`);
374
+ state.styleRegistry.set(ltrStyleKey, ltrStyleObject);
375
+
376
+ objectProperties.push(
377
+ t.objectProperty(
378
+ t.identifier("ltrStyle"),
379
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(ltrStyleKey)),
380
+ ),
381
+ );
382
+ }
383
+ }
384
+
385
+ // Group other modifiers by type (non-color-scheme, non-platform, and non-directional modifiers)
297
386
  const modifiersByType = new Map<ModifierType, ParsedModifier[]>();
298
387
  for (const mod of otherModifiers) {
299
388
  if (!modifiersByType.has(mod.modifier)) {
@@ -1 +1 @@
1
- var _interopRequireDefault=require("@babel/runtime/helpers/interopRequireDefault");Object.defineProperty(exports,"__esModule",{value:true});exports.BORDER_WIDTH_SCALE=exports.BORDER_RADIUS_SCALE=void 0;exports.parseBorder=parseBorder;var _defineProperty2=_interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));var _colors=require("./colors");var BORDER_WIDTH_SCALE=exports.BORDER_WIDTH_SCALE={"":1,"0":0,"2":2,"4":4,"8":8};var BORDER_RADIUS_SCALE=exports.BORDER_RADIUS_SCALE={none:0,sm:2,"":4,md:6,lg:8,xl:12,"2xl":16,"3xl":24,full:9999};var BORDER_WIDTH_PROP_MAP={t:"borderTopWidth",r:"borderRightWidth",b:"borderBottomWidth",l:"borderLeftWidth"};var BORDER_RADIUS_CORNER_MAP={tl:"borderTopLeftRadius",tr:"borderTopRightRadius",bl:"borderBottomLeftRadius",br:"borderBottomRightRadius"};var BORDER_RADIUS_SIDE_MAP={t:["borderTopLeftRadius","borderTopRightRadius"],r:["borderTopRightRadius","borderBottomRightRadius"],b:["borderBottomLeftRadius","borderBottomRightRadius"],l:["borderTopLeftRadius","borderBottomLeftRadius"]};function parseArbitraryBorderWidth(value){var pxMatch=value.match(/^\[(\d+)(?:px)?\]$/);if(pxMatch){return parseInt(pxMatch[1],10);}if(value.startsWith("[")&&value.endsWith("]")){if(process.env.NODE_ENV!=="production"){console.warn(`[react-native-tailwind] Unsupported arbitrary border width value: ${value}. Only px values are supported (e.g., [8px] or [8]).`);}return null;}return null;}function parseArbitraryBorderRadius(value){var pxMatch=value.match(/^\[(\d+)(?:px)?\]$/);if(pxMatch){return parseInt(pxMatch[1],10);}if(value.startsWith("[")&&value.endsWith("]")){if(process.env.NODE_ENV!=="production"){console.warn(`[react-native-tailwind] Unsupported arbitrary border radius value: ${value}. Only px values are supported (e.g., [12px] or [12]).`);}return null;}return null;}function parseBorder(cls,customColors){if(cls==="border-solid")return{borderStyle:"solid"};if(cls==="border-dotted")return{borderStyle:"dotted"};if(cls==="border-dashed")return{borderStyle:"dashed"};if(cls.startsWith("border-")){return parseBorderWidth(cls,customColors);}if(cls==="border"){return{borderWidth:1};}if(cls.startsWith("rounded")){return parseBorderRadius(cls);}return null;}function parseBorderWidth(cls,customColors){var dirMatch=cls.match(/^border-([trbl])(?:-(.+))?$/);if(dirMatch){var dir=dirMatch[1];var valueStr=dirMatch[2]||"";if(valueStr){var colorResult=(0,_colors.parseColor)(cls,customColors);if(colorResult!==null){return null;}}if(valueStr.startsWith("[")){var arbitraryValue=parseArbitraryBorderWidth(valueStr);if(arbitraryValue!==null){return(0,_defineProperty2.default)({},BORDER_WIDTH_PROP_MAP[dir],arbitraryValue);}return null;}var scaleValue=BORDER_WIDTH_SCALE[valueStr];if(scaleValue!==undefined){return(0,_defineProperty2.default)({},BORDER_WIDTH_PROP_MAP[dir],scaleValue);}return null;}var allMatch=cls.match(/^border-(\d+)$/);if(allMatch){var value=BORDER_WIDTH_SCALE[allMatch[1]];if(value!==undefined){return{borderWidth:value};}}var allArbMatch=cls.match(/^border-(\[.+\])$/);if(allArbMatch){var _arbitraryValue=parseArbitraryBorderWidth(allArbMatch[1]);if(_arbitraryValue!==null){return{borderWidth:_arbitraryValue};}}return null;}function parseBorderRadius(cls){var withoutPrefix=cls.substring(7);if(withoutPrefix===""){return{borderRadius:BORDER_RADIUS_SCALE[""]};}if(!withoutPrefix.startsWith("-")){return null;}var rest=withoutPrefix.substring(1);if(rest===""){return null;}var cornerMatch=rest.match(/^(tl|tr|bl|br)(?:-(.+))?$/);if(cornerMatch){var corner=cornerMatch[1];var valueStr=cornerMatch[2]||"";if(valueStr.startsWith("[")){var arbitraryValue=parseArbitraryBorderRadius(valueStr);if(arbitraryValue!==null){return(0,_defineProperty2.default)({},BORDER_RADIUS_CORNER_MAP[corner],arbitraryValue);}return null;}var _scaleValue=BORDER_RADIUS_SCALE[valueStr];if(_scaleValue!==undefined){return(0,_defineProperty2.default)({},BORDER_RADIUS_CORNER_MAP[corner],_scaleValue);}return null;}var sideMatch=rest.match(/^([trbl])(?:-(.+))?$/);if(sideMatch){var side=sideMatch[1];var _valueStr=sideMatch[2]||"";var value;if(_valueStr.startsWith("[")){var _arbitraryValue2=parseArbitraryBorderRadius(_valueStr);if(_arbitraryValue2!==null){value=_arbitraryValue2;}else{return null;}}else{value=BORDER_RADIUS_SCALE[_valueStr];}if(value!==undefined){var result={};BORDER_RADIUS_SIDE_MAP[side].forEach(function(prop){return result[prop]=value;});return result;}return null;}if(rest.startsWith("[")){var _arbitraryValue3=parseArbitraryBorderRadius(rest);if(_arbitraryValue3!==null){return{borderRadius:_arbitraryValue3};}return null;}var scaleValue=BORDER_RADIUS_SCALE[rest];if(scaleValue!==undefined){return{borderRadius:scaleValue};}return null;}
1
+ var _interopRequireDefault=require("@babel/runtime/helpers/interopRequireDefault");Object.defineProperty(exports,"__esModule",{value:true});exports.BORDER_WIDTH_SCALE=exports.BORDER_RADIUS_SCALE=void 0;exports.parseBorder=parseBorder;var _defineProperty2=_interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));var _colors=require("./colors");var BORDER_WIDTH_SCALE=exports.BORDER_WIDTH_SCALE={"":1,"0":0,"2":2,"4":4,"8":8};var BORDER_RADIUS_SCALE=exports.BORDER_RADIUS_SCALE={none:0,sm:2,"":4,md:6,lg:8,xl:12,"2xl":16,"3xl":24,full:9999};var BORDER_WIDTH_PROP_MAP={t:"borderTopWidth",r:"borderRightWidth",b:"borderBottomWidth",l:"borderLeftWidth",s:"borderStartWidth",e:"borderEndWidth"};var BORDER_RADIUS_CORNER_MAP={tl:"borderTopLeftRadius",tr:"borderTopRightRadius",bl:"borderBottomLeftRadius",br:"borderBottomRightRadius"};var BORDER_RADIUS_LOGICAL_CORNER_MAP={ss:"borderTopStartRadius",se:"borderTopEndRadius",es:"borderBottomStartRadius",ee:"borderBottomEndRadius"};var BORDER_RADIUS_SIDE_MAP={t:["borderTopLeftRadius","borderTopRightRadius"],r:["borderTopRightRadius","borderBottomRightRadius"],b:["borderBottomLeftRadius","borderBottomRightRadius"],l:["borderTopLeftRadius","borderBottomLeftRadius"],s:["borderTopStartRadius","borderBottomStartRadius"],e:["borderTopEndRadius","borderBottomEndRadius"]};function parseArbitraryBorderWidth(value){var pxMatch=value.match(/^\[(\d+)(?:px)?\]$/);if(pxMatch){return parseInt(pxMatch[1],10);}if(value.startsWith("[")&&value.endsWith("]")){if(process.env.NODE_ENV!=="production"){console.warn(`[react-native-tailwind] Unsupported arbitrary border width value: ${value}. Only px values are supported (e.g., [8px] or [8]).`);}return null;}return null;}function parseArbitraryBorderRadius(value){var pxMatch=value.match(/^\[(\d+)(?:px)?\]$/);if(pxMatch){return parseInt(pxMatch[1],10);}if(value.startsWith("[")&&value.endsWith("]")){if(process.env.NODE_ENV!=="production"){console.warn(`[react-native-tailwind] Unsupported arbitrary border radius value: ${value}. Only px values are supported (e.g., [12px] or [12]).`);}return null;}return null;}function parseBorder(cls,customColors){if(cls==="border-solid")return{borderStyle:"solid"};if(cls==="border-dotted")return{borderStyle:"dotted"};if(cls==="border-dashed")return{borderStyle:"dashed"};if(cls.startsWith("border-")){return parseBorderWidth(cls,customColors);}if(cls==="border"){return{borderWidth:1};}if(cls.startsWith("rounded")){return parseBorderRadius(cls);}return null;}function parseBorderWidth(cls,customColors){var dirMatch=cls.match(/^border-([trblse])(?:-(.+))?$/);if(dirMatch){var dir=dirMatch[1];var valueStr=dirMatch[2]||"";if(valueStr&&dir!=="s"&&dir!=="e"){var colorResult=(0,_colors.parseColor)(cls,customColors);if(colorResult!==null){return null;}}if(valueStr.startsWith("[")){var arbitraryValue=parseArbitraryBorderWidth(valueStr);if(arbitraryValue!==null){return(0,_defineProperty2.default)({},BORDER_WIDTH_PROP_MAP[dir],arbitraryValue);}return null;}var scaleValue=BORDER_WIDTH_SCALE[valueStr];if(scaleValue!==undefined){return(0,_defineProperty2.default)({},BORDER_WIDTH_PROP_MAP[dir],scaleValue);}return null;}var allMatch=cls.match(/^border-(\d+)$/);if(allMatch){var value=BORDER_WIDTH_SCALE[allMatch[1]];if(value!==undefined){return{borderWidth:value};}}var allArbMatch=cls.match(/^border-(\[.+\])$/);if(allArbMatch){var _arbitraryValue=parseArbitraryBorderWidth(allArbMatch[1]);if(_arbitraryValue!==null){return{borderWidth:_arbitraryValue};}}return null;}function parseBorderRadius(cls){var withoutPrefix=cls.substring(7);if(withoutPrefix===""){return{borderRadius:BORDER_RADIUS_SCALE[""]};}if(!withoutPrefix.startsWith("-")){return null;}var rest=withoutPrefix.substring(1);if(rest===""){return null;}var cornerMatch=rest.match(/^(tl|tr|bl|br)(?:-(.+))?$/);if(cornerMatch){var corner=cornerMatch[1];var valueStr=cornerMatch[2]||"";if(valueStr.startsWith("[")){var arbitraryValue=parseArbitraryBorderRadius(valueStr);if(arbitraryValue!==null){return(0,_defineProperty2.default)({},BORDER_RADIUS_CORNER_MAP[corner],arbitraryValue);}return null;}var _scaleValue=BORDER_RADIUS_SCALE[valueStr];if(_scaleValue!==undefined){return(0,_defineProperty2.default)({},BORDER_RADIUS_CORNER_MAP[corner],_scaleValue);}return null;}var logicalCornerMatch=rest.match(/^(ss|se|es|ee)(?:-(.+))?$/);if(logicalCornerMatch){var _corner=logicalCornerMatch[1];var _valueStr=logicalCornerMatch[2]||"";if(_valueStr.startsWith("[")){var _arbitraryValue2=parseArbitraryBorderRadius(_valueStr);if(_arbitraryValue2!==null){return(0,_defineProperty2.default)({},BORDER_RADIUS_LOGICAL_CORNER_MAP[_corner],_arbitraryValue2);}return null;}var _scaleValue2=BORDER_RADIUS_SCALE[_valueStr];if(_scaleValue2!==undefined){return(0,_defineProperty2.default)({},BORDER_RADIUS_LOGICAL_CORNER_MAP[_corner],_scaleValue2);}return null;}var sideMatch=rest.match(/^([trblse])(?:-(.+))?$/);if(sideMatch){var side=sideMatch[1];var _valueStr2=sideMatch[2]||"";var value;if(_valueStr2.startsWith("[")){var _arbitraryValue3=parseArbitraryBorderRadius(_valueStr2);if(_arbitraryValue3!==null){value=_arbitraryValue3;}else{return null;}}else{value=BORDER_RADIUS_SCALE[_valueStr2];}if(value!==undefined){var result={};BORDER_RADIUS_SIDE_MAP[side].forEach(function(prop){return result[prop]=value;});return result;}return null;}if(rest.startsWith("[")){var _arbitraryValue4=parseArbitraryBorderRadius(rest);if(_arbitraryValue4!==null){return{borderRadius:_arbitraryValue4};}return null;}var scaleValue=BORDER_RADIUS_SCALE[rest];if(scaleValue!==undefined){return{borderRadius:scaleValue};}return null;}