@mgcrea/react-native-tailwind 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +437 -10
  2. package/dist/babel/config-loader.ts +1 -23
  3. package/dist/babel/index.cjs +543 -150
  4. package/dist/babel/index.d.ts +27 -2
  5. package/dist/babel/index.test.ts +268 -0
  6. package/dist/babel/index.ts +352 -44
  7. package/dist/components/Pressable.d.ts +2 -0
  8. package/dist/components/TextInput.d.ts +2 -0
  9. package/dist/config/palettes.d.ts +302 -0
  10. package/dist/config/palettes.js +1 -0
  11. package/dist/index.d.ts +3 -0
  12. package/dist/index.js +1 -1
  13. package/dist/parser/__snapshots__/colors.test.js.snap +242 -90
  14. package/dist/parser/__snapshots__/transforms.test.js.snap +58 -0
  15. package/dist/parser/colors.js +1 -1
  16. package/dist/parser/colors.test.js +1 -1
  17. package/dist/parser/layout.js +1 -1
  18. package/dist/parser/layout.test.js +1 -1
  19. package/dist/parser/typography.js +1 -1
  20. package/dist/parser/typography.test.js +1 -1
  21. package/dist/runtime.cjs +2 -0
  22. package/dist/runtime.cjs.map +7 -0
  23. package/dist/runtime.d.ts +139 -0
  24. package/dist/runtime.js +2 -0
  25. package/dist/runtime.js.map +7 -0
  26. package/dist/runtime.test.js +1 -0
  27. package/dist/stubs/tw.d.ts +60 -0
  28. package/dist/stubs/tw.js +1 -0
  29. package/dist/utils/flattenColors.d.ts +16 -0
  30. package/dist/utils/flattenColors.js +1 -0
  31. package/dist/utils/flattenColors.test.js +1 -0
  32. package/dist/utils/modifiers.d.ts +29 -0
  33. package/dist/utils/modifiers.js +1 -0
  34. package/dist/utils/modifiers.test.js +1 -0
  35. package/dist/utils/styleKey.test.js +1 -0
  36. package/package.json +15 -3
  37. package/src/babel/config-loader.ts +1 -23
  38. package/src/babel/index.test.ts +268 -0
  39. package/src/babel/index.ts +352 -44
  40. package/src/components/Pressable.tsx +1 -0
  41. package/src/components/TextInput.tsx +1 -0
  42. package/src/config/palettes.ts +304 -0
  43. package/src/index.ts +5 -0
  44. package/src/parser/colors.test.ts +47 -31
  45. package/src/parser/colors.ts +5 -110
  46. package/src/parser/layout.test.ts +35 -0
  47. package/src/parser/layout.ts +26 -0
  48. package/src/parser/typography.test.ts +10 -0
  49. package/src/parser/typography.ts +8 -0
  50. package/src/runtime.test.ts +325 -0
  51. package/src/runtime.ts +280 -0
  52. package/src/stubs/tw.ts +80 -0
  53. package/src/utils/flattenColors.test.ts +361 -0
  54. package/src/utils/flattenColors.ts +32 -0
  55. package/src/utils/modifiers.test.ts +286 -0
  56. package/src/utils/modifiers.ts +63 -0
  57. package/src/utils/styleKey.test.ts +168 -0
@@ -17,22 +17,49 @@ import { parseClassName as parseClassNameFn, splitModifierClasses } from "../par
17
17
  import { generateStyleKey as generateStyleKeyFn } from "../utils/styleKey.js";
18
18
  import { extractCustomColors } from "./config-loader.js";
19
19
 
20
+ /**
21
+ * Plugin options
22
+ */
23
+ export type PluginOptions = {
24
+ /**
25
+ * List of JSX attribute names to transform (in addition to or instead of 'className')
26
+ * Supports exact matches and glob patterns:
27
+ * - Exact: 'className', 'containerClassName'
28
+ * - Glob: '*ClassName' (matches any attribute ending in 'ClassName')
29
+ *
30
+ * @default ['className', 'contentContainerClassName', 'columnWrapperClassName', 'ListHeaderComponentClassName', 'ListFooterComponentClassName']
31
+ */
32
+ attributes?: string[];
33
+
34
+ /**
35
+ * Custom identifier name for the generated StyleSheet constant
36
+ *
37
+ * @default '_twStyles'
38
+ */
39
+ stylesIdentifier?: string;
40
+ };
41
+
20
42
  type PluginState = PluginPass & {
21
43
  styleRegistry: Map<string, StyleObject>;
22
44
  hasClassNames: boolean;
23
45
  hasStyleSheetImport: boolean;
24
46
  customColors: Record<string, string>;
47
+ supportedAttributes: Set<string>;
48
+ attributePatterns: RegExp[];
49
+ stylesIdentifier: string;
50
+ // Track tw/twStyle imports from main package
51
+ twImportNames: Set<string>; // e.g., ['tw', 'twStyle'] or ['tw as customTw']
52
+ hasTwImport: boolean;
25
53
  };
26
54
 
27
- // Use a unique identifier to avoid conflicts with user's own styles
28
- const STYLES_IDENTIFIER = "_twStyles";
55
+ // Default identifier for the generated StyleSheet constant
56
+ const DEFAULT_STYLES_IDENTIFIER = "_twStyles";
29
57
 
30
58
  /**
31
- * Supported className-like attributes
59
+ * Default className-like attributes (used when no custom attributes are provided)
32
60
  */
33
- const SUPPORTED_CLASS_ATTRIBUTES = [
61
+ const DEFAULT_CLASS_ATTRIBUTES = [
34
62
  "className",
35
- "containerClassName",
36
63
  "contentContainerClassName",
37
64
  "columnWrapperClassName",
38
65
  "ListHeaderComponentClassName",
@@ -40,25 +67,56 @@ const SUPPORTED_CLASS_ATTRIBUTES = [
40
67
  ] as const;
41
68
 
42
69
  /**
43
- * Get the target style prop name based on the className attribute
70
+ * Build attribute matching structures from plugin options
71
+ * Separates exact matches from pattern-based matches
44
72
  */
45
- function getTargetStyleProp(attributeName: string): string {
46
- if (attributeName === "containerClassName") {
47
- return "containerStyle";
48
- }
49
- if (attributeName === "contentContainerClassName") {
50
- return "contentContainerStyle";
51
- }
52
- if (attributeName === "columnWrapperClassName") {
53
- return "columnWrapperStyle";
73
+ function buildAttributeMatchers(attributes: string[]): {
74
+ exactMatches: Set<string>;
75
+ patterns: RegExp[];
76
+ } {
77
+ const exactMatches = new Set<string>();
78
+ const patterns: RegExp[] = [];
79
+
80
+ for (const attr of attributes) {
81
+ if (attr.includes("*")) {
82
+ // Convert glob pattern to regex
83
+ // *ClassName -> /^.*ClassName$/
84
+ // container* -> /^container.*$/
85
+ const regexPattern = "^" + attr.replace(/\*/g, ".*") + "$";
86
+ patterns.push(new RegExp(regexPattern));
87
+ } else {
88
+ // Exact match
89
+ exactMatches.add(attr);
90
+ }
54
91
  }
55
- if (attributeName === "ListHeaderComponentClassName") {
56
- return "ListHeaderComponentStyle";
92
+
93
+ return { exactMatches, patterns };
94
+ }
95
+
96
+ /**
97
+ * Check if an attribute name matches the configured attributes
98
+ */
99
+ function isAttributeSupported(attributeName: string, exactMatches: Set<string>, patterns: RegExp[]): boolean {
100
+ // Check exact matches first (faster)
101
+ if (exactMatches.has(attributeName)) {
102
+ return true;
57
103
  }
58
- if (attributeName === "ListFooterComponentClassName") {
59
- return "ListFooterComponentStyle";
104
+
105
+ // Check pattern matches
106
+ for (const pattern of patterns) {
107
+ if (pattern.test(attributeName)) {
108
+ return true;
109
+ }
60
110
  }
61
- return "style";
111
+
112
+ return false;
113
+ }
114
+
115
+ /**
116
+ * Get the target style prop name based on the className attribute
117
+ */
118
+ function getTargetStyleProp(attributeName: string): string {
119
+ return attributeName.endsWith("ClassName") ? attributeName.replace("ClassName", "Style") : "style";
62
120
  }
63
121
 
64
122
  /**
@@ -169,7 +227,7 @@ function processTemplateLiteral(
169
227
  staticParts.push(cls);
170
228
 
171
229
  // Add to parts array
172
- parts.push(t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey)));
230
+ parts.push(t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(styleKey)));
173
231
  }
174
232
  }
175
233
 
@@ -268,7 +326,7 @@ function processStringOrExpression(node: any, state: PluginState, t: typeof Babe
268
326
  const styleKey = generateStyleKey(className);
269
327
  state.styleRegistry.set(styleKey, styleObject);
270
328
 
271
- return t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey));
329
+ return t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(styleKey));
272
330
  }
273
331
 
274
332
  // Handle nested expressions recursively
@@ -309,7 +367,7 @@ function processStaticClassNameWithModifiers(
309
367
  const baseStyleObject = parseClassName(baseClassName, state.customColors);
310
368
  const baseStyleKey = generateStyleKey(baseClassName);
311
369
  state.styleRegistry.set(baseStyleKey, baseStyleObject);
312
- baseStyleExpression = t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(baseStyleKey));
370
+ baseStyleExpression = t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey));
313
371
  }
314
372
 
315
373
  // Parse and register modifier classes
@@ -346,7 +404,7 @@ function processStaticClassNameWithModifiers(
346
404
  const conditionalExpression = t.logicalExpression(
347
405
  "&&",
348
406
  t.identifier(stateProperty),
349
- t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(modifierStyleKey)),
407
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(modifierStyleKey)),
350
408
  );
351
409
 
352
410
  styleArrayElements.push(conditionalExpression);
@@ -403,11 +461,78 @@ function createStyleFunction(styleExpression: any, modifierTypes: ModifierType[]
403
461
  return t.arrowFunctionExpression([param], styleExpression);
404
462
  }
405
463
 
406
- export default function reactNativeTailwindBabelPlugin({
407
- types: t,
408
- }: {
409
- types: typeof BabelTypes;
410
- }): PluginObj<PluginState> {
464
+ /**
465
+ * Process tw`...` or twStyle('...') call and replace with TwStyle object
466
+ * Generates: { style: styles._base, activeStyle: styles._active, ... }
467
+ */
468
+ function processTwCall(className: string, path: NodePath, state: PluginState, t: typeof BabelTypes): void {
469
+ const { baseClasses, modifierClasses } = splitModifierClasses(className);
470
+
471
+ // Build TwStyle object properties
472
+ const objectProperties: any[] = [];
473
+
474
+ // Parse and add base styles
475
+ if (baseClasses.length > 0) {
476
+ const baseClassName = baseClasses.join(" ");
477
+ const baseStyleObject = parseClassName(baseClassName, state.customColors);
478
+ const baseStyleKey = generateStyleKey(baseClassName);
479
+ state.styleRegistry.set(baseStyleKey, baseStyleObject);
480
+
481
+ objectProperties.push(
482
+ t.objectProperty(
483
+ t.identifier("style"),
484
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey)),
485
+ ),
486
+ );
487
+ } else {
488
+ // No base classes - add empty style object
489
+ objectProperties.push(t.objectProperty(t.identifier("style"), t.objectExpression([])));
490
+ }
491
+
492
+ // Group modifiers by type
493
+ const modifiersByType = new Map<ModifierType, ParsedModifier[]>();
494
+ for (const mod of modifierClasses) {
495
+ if (!modifiersByType.has(mod.modifier)) {
496
+ modifiersByType.set(mod.modifier, []);
497
+ }
498
+ const modGroup = modifiersByType.get(mod.modifier);
499
+ if (modGroup) {
500
+ modGroup.push(mod);
501
+ }
502
+ }
503
+
504
+ // Add modifier styles
505
+ for (const [modifierType, modifiers] of modifiersByType) {
506
+ const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
507
+ const modifierStyleObject = parseClassName(modifierClassNames, state.customColors);
508
+ const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
509
+ state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
510
+
511
+ // Map modifier type to property name: active -> activeStyle
512
+ const propertyName = `${modifierType}Style`;
513
+
514
+ objectProperties.push(
515
+ t.objectProperty(
516
+ t.identifier(propertyName),
517
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(modifierStyleKey)),
518
+ ),
519
+ );
520
+ }
521
+
522
+ // Replace the tw`...` or twStyle('...') with the object
523
+ const twStyleObject = t.objectExpression(objectProperties);
524
+ path.replaceWith(twStyleObject);
525
+ }
526
+
527
+ export default function reactNativeTailwindBabelPlugin(
528
+ { types: t }: { types: typeof BabelTypes },
529
+ options?: PluginOptions,
530
+ ): PluginObj<PluginState> {
531
+ // Build attribute matchers from options
532
+ const attributes = options?.attributes ?? [...DEFAULT_CLASS_ATTRIBUTES];
533
+ const { exactMatches, patterns } = buildAttributeMatchers(attributes);
534
+ const stylesIdentifier = options?.stylesIdentifier ?? DEFAULT_STYLES_IDENTIFIER;
535
+
411
536
  return {
412
537
  name: "react-native-tailwind",
413
538
 
@@ -418,12 +543,22 @@ export default function reactNativeTailwindBabelPlugin({
418
543
  state.styleRegistry = new Map();
419
544
  state.hasClassNames = false;
420
545
  state.hasStyleSheetImport = false;
546
+ state.supportedAttributes = exactMatches;
547
+ state.attributePatterns = patterns;
548
+ state.stylesIdentifier = stylesIdentifier;
549
+ state.twImportNames = new Set();
550
+ state.hasTwImport = false;
421
551
 
422
552
  // Load custom colors from tailwind.config.*
423
553
  state.customColors = extractCustomColors(state.file.opts.filename ?? "");
424
554
  },
425
555
 
426
556
  exit(path: NodePath, state: PluginState) {
557
+ // Remove tw/twStyle imports if they were used (and transformed)
558
+ if (state.hasTwImport) {
559
+ removeTwImports(path, t);
560
+ }
561
+
427
562
  // If no classNames were found, skip StyleSheet generation
428
563
  if (!state.hasClassNames || state.styleRegistry.size === 0) {
429
564
  return;
@@ -434,14 +569,17 @@ export default function reactNativeTailwindBabelPlugin({
434
569
  addStyleSheetImport(path, t);
435
570
  }
436
571
 
437
- // Generate and inject StyleSheet.create at the end of the file
438
- injectStyles(path, state.styleRegistry, t);
572
+ // Generate and inject StyleSheet.create at the beginning of the file (after imports)
573
+ // This ensures _twStyles is defined before any code that references it
574
+ injectStylesAtTop(path, state.styleRegistry, state.stylesIdentifier, t);
439
575
  },
440
576
  },
441
577
 
442
- // Check if StyleSheet is already imported
578
+ // Check if StyleSheet is already imported and track tw/twStyle imports
443
579
  ImportDeclaration(path: NodePath, state: PluginState) {
444
580
  const node = path.node as any;
581
+
582
+ // Track react-native StyleSheet import
445
583
  if (node.source.value === "react-native") {
446
584
  const specifiers = node.specifiers;
447
585
  const hasStyleSheet = specifiers.some((spec: any) => {
@@ -459,14 +597,127 @@ export default function reactNativeTailwindBabelPlugin({
459
597
  state.hasStyleSheetImport = true;
460
598
  }
461
599
  }
600
+
601
+ // Track tw/twStyle imports from main package (for compile-time transformation)
602
+ if (node.source.value === "@mgcrea/react-native-tailwind") {
603
+ const specifiers = node.specifiers;
604
+ specifiers.forEach((spec: any) => {
605
+ if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
606
+ const importedName = spec.imported.name;
607
+ if (importedName === "tw" || importedName === "twStyle") {
608
+ // Track the local name (could be renamed: import { tw as customTw })
609
+ const localName = spec.local.name;
610
+ state.twImportNames.add(localName);
611
+ state.hasTwImport = true;
612
+ }
613
+ }
614
+ });
615
+ }
616
+ },
617
+
618
+ // Handle tw`...` tagged template expressions
619
+ TaggedTemplateExpression(path: NodePath, state: PluginState) {
620
+ const node = path.node as any;
621
+
622
+ // Check if the tag is a tracked tw import
623
+ if (!t.isIdentifier(node.tag)) {
624
+ return;
625
+ }
626
+
627
+ const tagName = node.tag.name;
628
+ if (!state.twImportNames.has(tagName)) {
629
+ return;
630
+ }
631
+
632
+ // Extract static className from template literal
633
+ const quasi = node.quasi;
634
+ if (!t.isTemplateLiteral(quasi)) {
635
+ return;
636
+ }
637
+
638
+ // Only support static strings (no interpolations)
639
+ if (quasi.expressions.length > 0) {
640
+ if (process.env.NODE_ENV !== "production") {
641
+ console.warn(
642
+ `[react-native-tailwind] Dynamic tw\`...\` with interpolations is not supported at ${state.file.opts.filename ?? "unknown"}. ` +
643
+ `Use style prop for dynamic values.`,
644
+ );
645
+ }
646
+ return;
647
+ }
648
+
649
+ // Get the static className string
650
+ const className = quasi.quasis[0]?.value.cooked?.trim() ?? "";
651
+ if (!className) {
652
+ // Replace with empty object
653
+ path.replaceWith(
654
+ t.objectExpression([t.objectProperty(t.identifier("style"), t.objectExpression([]))]),
655
+ );
656
+ return;
657
+ }
658
+
659
+ state.hasClassNames = true;
660
+
661
+ // Process the className with modifiers
662
+ processTwCall(className, path, state, t);
663
+ },
664
+
665
+ // Handle twStyle('...') call expressions
666
+ CallExpression(path: NodePath, state: PluginState) {
667
+ const node = path.node as any;
668
+
669
+ // Check if the callee is a tracked twStyle import
670
+ if (!t.isIdentifier(node.callee)) {
671
+ return;
672
+ }
673
+
674
+ const calleeName = node.callee.name;
675
+ if (!state.twImportNames.has(calleeName)) {
676
+ return;
677
+ }
678
+
679
+ // Must have exactly one argument
680
+ if (node.arguments.length !== 1) {
681
+ if (process.env.NODE_ENV !== "production") {
682
+ console.warn(
683
+ `[react-native-tailwind] twStyle() expects exactly one argument at ${state.file.opts.filename ?? "unknown"}`,
684
+ );
685
+ }
686
+ return;
687
+ }
688
+
689
+ const arg = node.arguments[0];
690
+
691
+ // Only support static string literals
692
+ if (!t.isStringLiteral(arg)) {
693
+ if (process.env.NODE_ENV !== "production") {
694
+ console.warn(
695
+ `[react-native-tailwind] twStyle() only supports static string literals at ${state.file.opts.filename ?? "unknown"}. ` +
696
+ `Use style prop for dynamic values.`,
697
+ );
698
+ }
699
+ return;
700
+ }
701
+
702
+ const className = arg.value.trim();
703
+ if (!className) {
704
+ // Replace with undefined
705
+ path.replaceWith(t.identifier("undefined"));
706
+ return;
707
+ }
708
+
709
+ state.hasClassNames = true;
710
+
711
+ // Process the className with modifiers
712
+ processTwCall(className, path, state, t);
462
713
  },
463
714
 
464
715
  JSXAttribute(path: NodePath, state: PluginState) {
465
716
  const node = path.node as any;
466
717
  const attributeName = node.name.name;
467
718
 
468
- // Only process className-like attributes
469
- if (!SUPPORTED_CLASS_ATTRIBUTES.includes(attributeName)) {
719
+ // Only process configured className-like attributes
720
+ if (!isAttributeSupported(attributeName, state.supportedAttributes, state.attributePatterns)) {
470
721
  return;
471
722
  }
472
723
 
@@ -590,10 +841,10 @@ export default function reactNativeTailwindBabelPlugin({
590
841
 
591
842
  if (styleAttribute) {
592
843
  // Merge with existing style prop
593
- mergeStyleAttribute(path, styleAttribute, styleKey, t);
844
+ mergeStyleAttribute(path, styleAttribute, styleKey, state.stylesIdentifier, t);
594
845
  } else {
595
846
  // Replace className with style prop
596
- replaceWithStyleAttribute(path, styleKey, targetStyleProp, t);
847
+ replaceWithStyleAttribute(path, styleKey, targetStyleProp, state.stylesIdentifier, t);
597
848
  }
598
849
  return;
599
850
  }
@@ -665,6 +916,41 @@ function addStyleSheetImport(path: NodePath, t: typeof BabelTypes) {
665
916
  (path as any).unshiftContainer("body", importDeclaration);
666
917
  }
667
918
 
919
+ /**
920
+ * Remove tw/twStyle imports from @mgcrea/react-native-tailwind
921
+ * This is called after all tw calls have been transformed
922
+ */
923
+ function removeTwImports(path: NodePath, t: typeof BabelTypes) {
924
+ // Traverse the program to find and remove tw/twStyle imports
925
+ path.traverse({
926
+ ImportDeclaration(importPath: NodePath) {
927
+ const node = importPath.node as any;
928
+
929
+ // Only process imports from main package
930
+ if (node.source.value !== "@mgcrea/react-native-tailwind") {
931
+ return;
932
+ }
933
+
934
+ // Filter out tw/twStyle specifiers
935
+ const remainingSpecifiers = node.specifiers.filter((spec: any) => {
936
+ if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
937
+ const importedName = spec.imported.name;
938
+ return importedName !== "tw" && importedName !== "twStyle";
939
+ }
940
+ return true;
941
+ });
942
+
943
+ if (remainingSpecifiers.length === 0) {
944
+ // Remove entire import if no specifiers remain
945
+ importPath.remove();
946
+ } else if (remainingSpecifiers.length < node.specifiers.length) {
947
+ // Update import with remaining specifiers
948
+ node.specifiers = remainingSpecifiers;
949
+ }
950
+ },
951
+ });
952
+ }
953
+
668
954
  /**
669
955
  * Replace className with style attribute
670
956
  */
@@ -672,11 +958,12 @@ function replaceWithStyleAttribute(
672
958
  classNamePath: NodePath,
673
959
  styleKey: string,
674
960
  targetStyleProp: string,
961
+ stylesIdentifier: string,
675
962
  t: typeof BabelTypes,
676
963
  ) {
677
964
  const styleAttribute = t.jsxAttribute(
678
965
  t.jsxIdentifier(targetStyleProp),
679
- t.jsxExpressionContainer(t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey))),
966
+ t.jsxExpressionContainer(t.memberExpression(t.identifier(stylesIdentifier), t.identifier(styleKey))),
680
967
  );
681
968
 
682
969
  classNamePath.replaceWith(styleAttribute);
@@ -689,6 +976,7 @@ function mergeStyleAttribute(
689
976
  classNamePath: NodePath,
690
977
  styleAttribute: any,
691
978
  styleKey: string,
979
+ stylesIdentifier: string,
692
980
  t: typeof BabelTypes,
693
981
  ) {
694
982
  const existingStyle = styleAttribute.value.expression;
@@ -696,7 +984,7 @@ function mergeStyleAttribute(
696
984
  // Create array with className styles first, then existing styles
697
985
  // This allows existing styles to override className styles
698
986
  const styleArray = t.arrayExpression([
699
- t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey)),
987
+ t.memberExpression(t.identifier(stylesIdentifier), t.identifier(styleKey)),
700
988
  existingStyle,
701
989
  ]);
702
990
 
@@ -815,9 +1103,15 @@ function mergeStyleFunctionAttribute(
815
1103
  }
816
1104
 
817
1105
  /**
818
- * Inject StyleSheet.create with all collected styles
1106
+ * Inject StyleSheet.create with all collected styles at the top of the file
1107
+ * This ensures the styles object is defined before any code that references it
819
1108
  */
820
- function injectStyles(path: NodePath, styleRegistry: Map<string, StyleObject>, t: typeof BabelTypes) {
1109
+ function injectStylesAtTop(
1110
+ path: NodePath,
1111
+ styleRegistry: Map<string, StyleObject>,
1112
+ stylesIdentifier: string,
1113
+ t: typeof BabelTypes,
1114
+ ) {
821
1115
  // Build style object properties
822
1116
  const styleProperties: any[] = [];
823
1117
 
@@ -840,18 +1134,32 @@ function injectStyles(path: NodePath, styleRegistry: Map<string, StyleObject>, t
840
1134
  styleProperties.push(t.objectProperty(t.identifier(key), t.objectExpression(properties)));
841
1135
  }
842
1136
 
843
- // Create: const _tailwindStyles = StyleSheet.create({ ... })
1137
+ // Create: const _twStyles = StyleSheet.create({ ... })
844
1138
  const styleSheet = t.variableDeclaration("const", [
845
1139
  t.variableDeclarator(
846
- t.identifier(STYLES_IDENTIFIER),
1140
+ t.identifier(stylesIdentifier),
847
1141
  t.callExpression(t.memberExpression(t.identifier("StyleSheet"), t.identifier("create")), [
848
1142
  t.objectExpression(styleProperties),
849
1143
  ]),
850
1144
  ),
851
1145
  ]);
852
1146
 
853
- // Add StyleSheet.create at the end of the file
854
- (path as any).pushContainer("body", styleSheet);
1147
+ // Find the index to insert after all imports
1148
+ const body = (path as any).node.body;
1149
+ let insertIndex = 0;
1150
+
1151
+ // Find the last import statement
1152
+ for (let i = 0; i < body.length; i++) {
1153
+ if (t.isImportDeclaration(body[i])) {
1154
+ insertIndex = i + 1;
1155
+ } else {
1156
+ // Stop at the first non-import statement
1157
+ break;
1158
+ }
1159
+ }
1160
+
1161
+ // Insert StyleSheet.create after imports
1162
+ body.splice(insertIndex, 0, styleSheet);
855
1163
  }
856
1164
 
857
1165
  // Helper functions that use the imported parser
@@ -11,6 +11,7 @@ export type PressableProps = Omit<RNPressableProps, "style"> & {
11
11
  * Style can be a static style object/array or a function that receives Pressable state + disabled
12
12
  */
13
13
  style?: StyleProp<ViewStyle> | ((state: EnhancedPressableState) => StyleProp<ViewStyle>);
14
+ className?: string;
14
15
  };
15
16
  /**
16
17
  * Enhanced Pressable that supports the disabled: modifier
@@ -28,5 +29,6 @@ export declare const Pressable: import("react").ForwardRefExoticComponent<Omit<R
28
29
  * Style can be a static style object/array or a function that receives Pressable state + disabled
29
30
  */
30
31
  style?: StyleProp<ViewStyle> | ((state: EnhancedPressableState) => StyleProp<ViewStyle>);
32
+ className?: string;
31
33
  } & import("react").RefAttributes<import("react-native").View>>;
32
34
  export {};
@@ -23,6 +23,7 @@ export type TextInputProps = Omit<RNTextInputProps, "style"> & {
23
23
  focused: boolean;
24
24
  disabled: boolean;
25
25
  }) => RNTextInputProps["style"]);
26
+ className?: string;
26
27
  /**
27
28
  * Convenience prop for disabled state (overrides editable if provided)
28
29
  * When true, sets editable to false
@@ -48,6 +49,7 @@ export declare const TextInput: import("react").ForwardRefExoticComponent<Omit<R
48
49
  focused: boolean;
49
50
  disabled: boolean;
50
51
  }) => RNTextInputProps["style"]);
52
+ className?: string;
51
53
  /**
52
54
  * Convenience prop for disabled state (overrides editable if provided)
53
55
  * When true, sets editable to false