@mgcrea/react-native-tailwind 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +459 -39
  2. package/dist/babel/index.cjs +810 -279
  3. package/dist/babel/index.d.ts +2 -1
  4. package/dist/babel/index.ts +328 -22
  5. package/dist/components/Pressable.d.ts +32 -0
  6. package/dist/components/Pressable.js +1 -0
  7. package/dist/components/TextInput.d.ts +56 -0
  8. package/dist/components/TextInput.js +1 -0
  9. package/dist/index.d.ts +9 -2
  10. package/dist/index.js +1 -1
  11. package/dist/parser/aspectRatio.d.ts +16 -0
  12. package/dist/parser/aspectRatio.js +1 -0
  13. package/dist/parser/aspectRatio.test.d.ts +1 -0
  14. package/dist/parser/aspectRatio.test.js +1 -0
  15. package/dist/parser/borders.js +1 -1
  16. package/dist/parser/borders.test.d.ts +1 -0
  17. package/dist/parser/borders.test.js +1 -0
  18. package/dist/parser/colors.d.ts +1 -0
  19. package/dist/parser/colors.js +1 -1
  20. package/dist/parser/colors.test.d.ts +1 -0
  21. package/dist/parser/colors.test.js +1 -0
  22. package/dist/parser/index.d.ts +4 -0
  23. package/dist/parser/index.js +1 -1
  24. package/dist/parser/layout.d.ts +2 -0
  25. package/dist/parser/layout.js +1 -1
  26. package/dist/parser/layout.test.d.ts +1 -0
  27. package/dist/parser/layout.test.js +1 -0
  28. package/dist/parser/modifiers.d.ts +47 -0
  29. package/dist/parser/modifiers.js +1 -0
  30. package/dist/parser/modifiers.test.d.ts +1 -0
  31. package/dist/parser/modifiers.test.js +1 -0
  32. package/dist/parser/shadows.d.ts +26 -0
  33. package/dist/parser/shadows.js +1 -0
  34. package/dist/parser/shadows.test.d.ts +1 -0
  35. package/dist/parser/shadows.test.js +1 -0
  36. package/dist/parser/sizing.test.d.ts +1 -0
  37. package/dist/parser/sizing.test.js +1 -0
  38. package/dist/parser/spacing.d.ts +1 -1
  39. package/dist/parser/spacing.js +1 -1
  40. package/dist/parser/spacing.test.d.ts +1 -0
  41. package/dist/parser/spacing.test.js +1 -0
  42. package/dist/parser/typography.d.ts +2 -1
  43. package/dist/parser/typography.js +1 -1
  44. package/dist/parser/typography.test.d.ts +1 -0
  45. package/dist/parser/typography.test.js +1 -0
  46. package/dist/types.d.ts +5 -2
  47. package/package.json +7 -6
  48. package/src/babel/index.ts +328 -22
  49. package/src/components/Pressable.tsx +46 -0
  50. package/src/components/TextInput.tsx +90 -0
  51. package/src/index.ts +20 -2
  52. package/src/parser/aspectRatio.test.ts +191 -0
  53. package/src/parser/aspectRatio.ts +73 -0
  54. package/src/parser/borders.test.ts +329 -0
  55. package/src/parser/borders.ts +187 -108
  56. package/src/parser/colors.test.ts +335 -0
  57. package/src/parser/colors.ts +117 -6
  58. package/src/parser/index.ts +13 -2
  59. package/src/parser/layout.test.ts +459 -0
  60. package/src/parser/layout.ts +128 -0
  61. package/src/parser/modifiers.test.ts +375 -0
  62. package/src/parser/modifiers.ts +104 -0
  63. package/src/parser/shadows.test.ts +201 -0
  64. package/src/parser/shadows.ts +133 -0
  65. package/src/parser/sizing.test.ts +256 -0
  66. package/src/parser/spacing.test.ts +226 -0
  67. package/src/parser/spacing.ts +93 -138
  68. package/src/parser/typography.test.ts +221 -0
  69. package/src/parser/typography.ts +143 -112
  70. package/src/types.ts +2 -2
  71. package/dist/react-native.d.js +0 -1
@@ -0,0 +1 @@
1
+ var _vitest=require("vitest");var _typography=require("./typography");(0,_vitest.describe)("FONT_SIZES",function(){(0,_vitest.it)("should export complete font size scale",function(){(0,_vitest.expect)(_typography.FONT_SIZES).toMatchSnapshot();});});(0,_vitest.describe)("LETTER_SPACING_SCALE",function(){(0,_vitest.it)("should export complete letter spacing scale",function(){(0,_vitest.expect)(_typography.LETTER_SPACING_SCALE).toMatchSnapshot();});});(0,_vitest.describe)("parseTypography - font size",function(){(0,_vitest.it)("should parse font size with preset values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("text-xs")).toEqual({fontSize:12});(0,_vitest.expect)((0,_typography.parseTypography)("text-sm")).toEqual({fontSize:14});(0,_vitest.expect)((0,_typography.parseTypography)("text-base")).toEqual({fontSize:16});(0,_vitest.expect)((0,_typography.parseTypography)("text-lg")).toEqual({fontSize:18});(0,_vitest.expect)((0,_typography.parseTypography)("text-xl")).toEqual({fontSize:20});(0,_vitest.expect)((0,_typography.parseTypography)("text-2xl")).toEqual({fontSize:24});(0,_vitest.expect)((0,_typography.parseTypography)("text-3xl")).toEqual({fontSize:30});(0,_vitest.expect)((0,_typography.parseTypography)("text-4xl")).toEqual({fontSize:36});(0,_vitest.expect)((0,_typography.parseTypography)("text-5xl")).toEqual({fontSize:48});(0,_vitest.expect)((0,_typography.parseTypography)("text-6xl")).toEqual({fontSize:60});(0,_vitest.expect)((0,_typography.parseTypography)("text-7xl")).toEqual({fontSize:72});(0,_vitest.expect)((0,_typography.parseTypography)("text-8xl")).toEqual({fontSize:96});(0,_vitest.expect)((0,_typography.parseTypography)("text-9xl")).toEqual({fontSize:128});});(0,_vitest.it)("should parse font size with arbitrary pixel values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("text-[18px]")).toEqual({fontSize:18});(0,_vitest.expect)((0,_typography.parseTypography)("text-[18]")).toEqual({fontSize:18});(0,_vitest.expect)((0,_typography.parseTypography)("text-[22px]")).toEqual({fontSize:22});(0,_vitest.expect)((0,_typography.parseTypography)("text-[22]")).toEqual({fontSize:22});(0,_vitest.expect)((0,_typography.parseTypography)("text-[100px]")).toEqual({fontSize:100});});});(0,_vitest.describe)("parseTypography - font weight",function(){(0,_vitest.it)("should parse font weight values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("font-thin")).toEqual({fontWeight:"100"});(0,_vitest.expect)((0,_typography.parseTypography)("font-extralight")).toEqual({fontWeight:"200"});(0,_vitest.expect)((0,_typography.parseTypography)("font-light")).toEqual({fontWeight:"300"});(0,_vitest.expect)((0,_typography.parseTypography)("font-normal")).toEqual({fontWeight:"400"});(0,_vitest.expect)((0,_typography.parseTypography)("font-medium")).toEqual({fontWeight:"500"});(0,_vitest.expect)((0,_typography.parseTypography)("font-semibold")).toEqual({fontWeight:"600"});(0,_vitest.expect)((0,_typography.parseTypography)("font-bold")).toEqual({fontWeight:"700"});(0,_vitest.expect)((0,_typography.parseTypography)("font-extrabold")).toEqual({fontWeight:"800"});(0,_vitest.expect)((0,_typography.parseTypography)("font-black")).toEqual({fontWeight:"900"});});});(0,_vitest.describe)("parseTypography - font style",function(){(0,_vitest.it)("should parse font style values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("italic")).toEqual({fontStyle:"italic"});(0,_vitest.expect)((0,_typography.parseTypography)("not-italic")).toEqual({fontStyle:"normal"});});});(0,_vitest.describe)("parseTypography - text alignment",function(){(0,_vitest.it)("should parse text alignment values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("text-left")).toEqual({textAlign:"left"});(0,_vitest.expect)((0,_typography.parseTypography)("text-center")).toEqual({textAlign:"center"});(0,_vitest.expect)((0,_typography.parseTypography)("text-right")).toEqual({textAlign:"right"});(0,_vitest.expect)((0,_typography.parseTypography)("text-justify")).toEqual({textAlign:"justify"});});});(0,_vitest.describe)("parseTypography - text decoration",function(){(0,_vitest.it)("should parse text decoration values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("underline")).toEqual({textDecorationLine:"underline"});(0,_vitest.expect)((0,_typography.parseTypography)("line-through")).toEqual({textDecorationLine:"line-through"});(0,_vitest.expect)((0,_typography.parseTypography)("no-underline")).toEqual({textDecorationLine:"none"});});});(0,_vitest.describe)("parseTypography - text transform",function(){(0,_vitest.it)("should parse text transform values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("uppercase")).toEqual({textTransform:"uppercase"});(0,_vitest.expect)((0,_typography.parseTypography)("lowercase")).toEqual({textTransform:"lowercase"});(0,_vitest.expect)((0,_typography.parseTypography)("capitalize")).toEqual({textTransform:"capitalize"});(0,_vitest.expect)((0,_typography.parseTypography)("normal-case")).toEqual({textTransform:"none"});});});(0,_vitest.describe)("parseTypography - line height",function(){(0,_vitest.it)("should parse line height with preset values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("leading-none")).toEqual({lineHeight:16});(0,_vitest.expect)((0,_typography.parseTypography)("leading-tight")).toEqual({lineHeight:20});(0,_vitest.expect)((0,_typography.parseTypography)("leading-snug")).toEqual({lineHeight:22});(0,_vitest.expect)((0,_typography.parseTypography)("leading-normal")).toEqual({lineHeight:24});(0,_vitest.expect)((0,_typography.parseTypography)("leading-relaxed")).toEqual({lineHeight:28});(0,_vitest.expect)((0,_typography.parseTypography)("leading-loose")).toEqual({lineHeight:32});});(0,_vitest.it)("should parse line height with arbitrary pixel values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("leading-[24px]")).toEqual({lineHeight:24});(0,_vitest.expect)((0,_typography.parseTypography)("leading-[24]")).toEqual({lineHeight:24});(0,_vitest.expect)((0,_typography.parseTypography)("leading-[30px]")).toEqual({lineHeight:30});(0,_vitest.expect)((0,_typography.parseTypography)("leading-[30]")).toEqual({lineHeight:30});(0,_vitest.expect)((0,_typography.parseTypography)("leading-[50px]")).toEqual({lineHeight:50});});});(0,_vitest.describe)("parseTypography - letter spacing",function(){(0,_vitest.it)("should parse letter spacing with preset values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("tracking-tighter")).toEqual({letterSpacing:-0.8});(0,_vitest.expect)((0,_typography.parseTypography)("tracking-tight")).toEqual({letterSpacing:-0.4});(0,_vitest.expect)((0,_typography.parseTypography)("tracking-normal")).toEqual({letterSpacing:0});(0,_vitest.expect)((0,_typography.parseTypography)("tracking-wide")).toEqual({letterSpacing:0.4});(0,_vitest.expect)((0,_typography.parseTypography)("tracking-wider")).toEqual({letterSpacing:0.8});(0,_vitest.expect)((0,_typography.parseTypography)("tracking-widest")).toEqual({letterSpacing:1.6});});});(0,_vitest.describe)("parseTypography - edge cases",function(){(0,_vitest.it)("should return null for invalid classes",function(){(0,_vitest.expect)((0,_typography.parseTypography)("invalid")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("text")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("font")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("leading")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("tracking")).toBeNull();});(0,_vitest.it)("should return null for invalid font size values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("text-invalid")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("text-10xl")).toBeNull();});(0,_vitest.it)("should return null for invalid font weight values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("font-invalid")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("font-100")).toBeNull();});(0,_vitest.it)("should return null for arbitrary values with unsupported units",function(){(0,_vitest.expect)((0,_typography.parseTypography)("text-[16rem]")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("text-[2em]")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("leading-[2rem]")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("leading-[1.5em]")).toBeNull();});(0,_vitest.it)("should return null for malformed arbitrary values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("text-[16")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("text-16]")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("text-[]")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("leading-[24")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("leading-24]")).toBeNull();});(0,_vitest.it)("should not match partial class names",function(){(0,_vitest.expect)((0,_typography.parseTypography)("mytext-lg")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("font-bold-extra")).toBeNull();(0,_vitest.expect)((0,_typography.parseTypography)("italic-text")).toBeNull();});});(0,_vitest.describe)("parseTypography - comprehensive coverage",function(){(0,_vitest.it)("should handle all typography categories independently",function(){(0,_vitest.expect)((0,_typography.parseTypography)("text-base")).toEqual({fontSize:16});(0,_vitest.expect)((0,_typography.parseTypography)("font-bold")).toEqual({fontWeight:"700"});(0,_vitest.expect)((0,_typography.parseTypography)("italic")).toEqual({fontStyle:"italic"});(0,_vitest.expect)((0,_typography.parseTypography)("text-center")).toEqual({textAlign:"center"});(0,_vitest.expect)((0,_typography.parseTypography)("underline")).toEqual({textDecorationLine:"underline"});(0,_vitest.expect)((0,_typography.parseTypography)("uppercase")).toEqual({textTransform:"uppercase"});(0,_vitest.expect)((0,_typography.parseTypography)("leading-normal")).toEqual({lineHeight:24});(0,_vitest.expect)((0,_typography.parseTypography)("tracking-wide")).toEqual({letterSpacing:0.4});});(0,_vitest.it)("should handle arbitrary values for font size and line height",function(){(0,_vitest.expect)((0,_typography.parseTypography)("text-[19px]")).toEqual({fontSize:19});(0,_vitest.expect)((0,_typography.parseTypography)("text-[19]")).toEqual({fontSize:19});(0,_vitest.expect)((0,_typography.parseTypography)("leading-[26px]")).toEqual({lineHeight:26});(0,_vitest.expect)((0,_typography.parseTypography)("leading-[26]")).toEqual({lineHeight:26});});(0,_vitest.it)("should handle edge case values",function(){(0,_vitest.expect)((0,_typography.parseTypography)("text-xs")).toEqual({fontSize:12});(0,_vitest.expect)((0,_typography.parseTypography)("text-9xl")).toEqual({fontSize:128});(0,_vitest.expect)((0,_typography.parseTypography)("leading-none")).toEqual({lineHeight:16});(0,_vitest.expect)((0,_typography.parseTypography)("leading-loose")).toEqual({lineHeight:32});(0,_vitest.expect)((0,_typography.parseTypography)("tracking-tighter")).toEqual({letterSpacing:-0.8});(0,_vitest.expect)((0,_typography.parseTypography)("tracking-widest")).toEqual({letterSpacing:1.6});});});
package/dist/types.d.ts CHANGED
@@ -3,8 +3,11 @@
3
3
  */
4
4
  import type { ImageStyle, TextStyle, ViewStyle } from "react-native";
5
5
  export type RNStyle = ViewStyle | TextStyle | ImageStyle;
6
- export type StyleObject = {
7
- [key: string]: string | number;
6
+ export type StyleObject = Record<Exclude<string, "shadowOffset">, string | number | undefined> & {
7
+ shadowOffset?: {
8
+ width: number;
9
+ height: number;
10
+ };
8
11
  };
9
12
  export type SpacingValue = number;
10
13
  export type ColorValue = string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mgcrea/react-native-tailwind",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Compile-time Tailwind CSS for React Native with zero runtime overhead",
5
5
  "author": "Olivier Louvignes <olivier@mgcrea.io> (https://github.com/mgcrea)",
6
6
  "homepage": "https://github.com/mgcrea/react-native-tailwind#readme",
@@ -33,14 +33,14 @@
33
33
  "dev": "cd example; npm run dev",
34
34
  "build": "npm run build:babel && npm run build:babel-plugin && npm run build:types",
35
35
  "build:babel": "babel src --out-dir dist --extensions \".ts,.tsx,.js,.jsx\" --copy-files --ignore 'src/babel/**' --ignore '**/*.d.ts'",
36
- "build:babel-plugin": "node scripts/bundle-babel-plugin.cjs",
37
- "build:types": "tsc --emitDeclarationOnly && node scripts/post-build-types.cjs",
36
+ "build:babel-plugin": "node --experimental-strip-types scripts/bundle-babel-plugin.ts",
37
+ "build:types": "tsc --project tsconfig.build.json --emitDeclarationOnly && node --experimental-strip-types scripts/post-build-types.ts",
38
38
  "install:ios": "cd example; npm run install:ios",
39
39
  "open:ios": "cd example; npm run open:ios",
40
- "lint": "eslint .",
40
+ "lint": "eslint src/",
41
41
  "prettify": "prettier --write src/",
42
42
  "check": "tsc --noEmit",
43
- "spec": "NODE_OPTIONS=--experimental-require-module jest",
43
+ "spec": "vitest",
44
44
  "test": "npm run lint && npm run check && npm run spec",
45
45
  "prepare": "npm run build"
46
46
  },
@@ -71,7 +71,8 @@
71
71
  "prettier-plugin-organize-imports": "^4.3.0",
72
72
  "react": "^19.2.0",
73
73
  "react-native": "0.82.1",
74
- "typescript": "^5.9.3"
74
+ "typescript": "^5.9.3",
75
+ "vitest": "^4.0.10"
75
76
  },
76
77
  "engines": {
77
78
  "node": ">=18"
@@ -11,22 +11,28 @@
11
11
 
12
12
  import type { NodePath, PluginObj, PluginPass } from "@babel/core";
13
13
  import * as BabelTypes from "@babel/types";
14
- import { parseClassName as parseClassNameFn } from "../parser/index.js";
14
+ import { StyleObject } from "src/types.js";
15
+ import type { ModifierType, ParsedModifier } from "../parser/index.js";
16
+ import { parseClassName as parseClassNameFn, splitModifierClasses } from "../parser/index.js";
15
17
  import { generateStyleKey as generateStyleKeyFn } from "../utils/styleKey.js";
16
18
  import { extractCustomColors } from "./config-loader.js";
17
19
 
18
20
  type PluginState = PluginPass & {
19
- styleRegistry: Map<string, Record<string, string | number>>;
21
+ styleRegistry: Map<string, StyleObject>;
20
22
  hasClassNames: boolean;
21
23
  hasStyleSheetImport: boolean;
22
24
  customColors: Record<string, string>;
23
25
  };
24
26
 
27
+ // Use a unique identifier to avoid conflicts with user's own styles
28
+ const STYLES_IDENTIFIER = "_twStyles";
29
+
25
30
  /**
26
31
  * Supported className-like attributes
27
32
  */
28
33
  const SUPPORTED_CLASS_ATTRIBUTES = [
29
34
  "className",
35
+ "containerClassName",
30
36
  "contentContainerClassName",
31
37
  "columnWrapperClassName",
32
38
  "ListHeaderComponentClassName",
@@ -37,6 +43,9 @@ const SUPPORTED_CLASS_ATTRIBUTES = [
37
43
  * Get the target style prop name based on the className attribute
38
44
  */
39
45
  function getTargetStyleProp(attributeName: string): string {
46
+ if (attributeName === "containerClassName") {
47
+ return "containerStyle";
48
+ }
40
49
  if (attributeName === "contentContainerClassName") {
41
50
  return "contentContainerStyle";
42
51
  }
@@ -52,6 +61,49 @@ function getTargetStyleProp(attributeName: string): string {
52
61
  return "style";
53
62
  }
54
63
 
64
+ /**
65
+ * Check if a JSX element supports modifiers and determine which modifiers are supported
66
+ * Returns an object with component info and supported modifiers
67
+ */
68
+ function getComponentModifierSupport(
69
+ jsxElement: any,
70
+ t: typeof BabelTypes,
71
+ ): { component: string; supportedModifiers: ModifierType[] } | null {
72
+ if (!t.isJSXOpeningElement(jsxElement)) {
73
+ return null;
74
+ }
75
+
76
+ const name = jsxElement.name;
77
+ let componentName: string | null = null;
78
+
79
+ // Handle simple identifier: <Pressable>
80
+ if (t.isJSXIdentifier(name)) {
81
+ componentName = name.name;
82
+ }
83
+
84
+ // Handle member expression: <ReactNative.Pressable>
85
+ if (t.isJSXMemberExpression(name)) {
86
+ const property = name.property;
87
+ if (t.isJSXIdentifier(property)) {
88
+ componentName = property.name;
89
+ }
90
+ }
91
+
92
+ if (!componentName) {
93
+ return null;
94
+ }
95
+
96
+ // Map components to their supported modifiers
97
+ switch (componentName) {
98
+ case "Pressable":
99
+ return { component: "Pressable", supportedModifiers: ["active", "hover", "focus", "disabled"] };
100
+ case "TextInput":
101
+ return { component: "TextInput", supportedModifiers: ["focus", "disabled"] };
102
+ default:
103
+ return null;
104
+ }
105
+ }
106
+
55
107
  /**
56
108
  * Result of processing a dynamic expression
57
109
  */
@@ -117,7 +169,7 @@ function processTemplateLiteral(
117
169
  staticParts.push(cls);
118
170
 
119
171
  // Add to parts array
120
- parts.push(t.memberExpression(t.identifier("styles"), t.identifier(styleKey)));
172
+ parts.push(t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey)));
121
173
  }
122
174
  }
123
175
 
@@ -216,7 +268,7 @@ function processStringOrExpression(node: any, state: PluginState, t: typeof Babe
216
268
  const styleKey = generateStyleKey(className);
217
269
  state.styleRegistry.set(styleKey, styleObject);
218
270
 
219
- return t.memberExpression(t.identifier("styles"), t.identifier(styleKey));
271
+ return t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey));
220
272
  }
221
273
 
222
274
  // Handle nested expressions recursively
@@ -239,6 +291,118 @@ function processStringOrExpression(node: any, state: PluginState, t: typeof Babe
239
291
  return null;
240
292
  }
241
293
 
294
+ /**
295
+ * Process a static className string that contains modifiers
296
+ * Returns a style function expression for Pressable components
297
+ */
298
+ function processStaticClassNameWithModifiers(
299
+ className: string,
300
+ state: PluginState,
301
+ t: typeof BabelTypes,
302
+ ): any {
303
+ const { baseClasses, modifierClasses } = splitModifierClasses(className);
304
+
305
+ // Parse and register base classes
306
+ let baseStyleExpression: any = null;
307
+ if (baseClasses.length > 0) {
308
+ const baseClassName = baseClasses.join(" ");
309
+ const baseStyleObject = parseClassName(baseClassName, state.customColors);
310
+ const baseStyleKey = generateStyleKey(baseClassName);
311
+ state.styleRegistry.set(baseStyleKey, baseStyleObject);
312
+ baseStyleExpression = t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(baseStyleKey));
313
+ }
314
+
315
+ // Parse and register modifier classes
316
+ // Group by modifier type for better organization
317
+ const modifiersByType = new Map<ModifierType, ParsedModifier[]>();
318
+ for (const mod of modifierClasses) {
319
+ if (!modifiersByType.has(mod.modifier)) {
320
+ modifiersByType.set(mod.modifier, []);
321
+ }
322
+ const modGroup = modifiersByType.get(mod.modifier);
323
+ if (modGroup) {
324
+ modGroup.push(mod);
325
+ }
326
+ }
327
+
328
+ // Build style function: ({ pressed }) => [baseStyle, pressed && modifierStyle]
329
+ const styleArrayElements: any[] = [];
330
+
331
+ // Add base style first
332
+ if (baseStyleExpression) {
333
+ styleArrayElements.push(baseStyleExpression);
334
+ }
335
+
336
+ // Add conditional styles for each modifier type
337
+ for (const [modifierType, modifiers] of modifiersByType) {
338
+ // Parse all modifier classes together
339
+ const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
340
+ const modifierStyleObject = parseClassName(modifierClassNames, state.customColors);
341
+ const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
342
+ state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
343
+
344
+ // Create conditional: pressed && styles._active_bg_blue_700
345
+ const stateProperty = getStatePropertyForModifier(modifierType);
346
+ const conditionalExpression = t.logicalExpression(
347
+ "&&",
348
+ t.identifier(stateProperty),
349
+ t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(modifierStyleKey)),
350
+ );
351
+
352
+ styleArrayElements.push(conditionalExpression);
353
+ }
354
+
355
+ // If only base style, return it directly; otherwise return array
356
+ if (styleArrayElements.length === 1) {
357
+ return styleArrayElements[0];
358
+ }
359
+
360
+ return t.arrayExpression(styleArrayElements);
361
+ }
362
+
363
+ /**
364
+ * Get the state property name for a modifier type
365
+ * Maps modifier types to component state parameter properties
366
+ */
367
+ function getStatePropertyForModifier(modifier: ModifierType): string {
368
+ switch (modifier) {
369
+ case "active":
370
+ return "pressed";
371
+ case "hover":
372
+ return "hovered";
373
+ case "focus":
374
+ return "focused";
375
+ case "disabled":
376
+ return "disabled";
377
+ default:
378
+ return "pressed"; // fallback
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Create a style function for Pressable: ({ pressed }) => styleExpression
384
+ */
385
+ function createStyleFunction(styleExpression: any, modifierTypes: ModifierType[], t: typeof BabelTypes): any {
386
+ // Build parameter object: { pressed, hovered, focused }
387
+ const paramProperties: any[] = [];
388
+ const usedStateProps = new Set<string>();
389
+
390
+ for (const modifierType of modifierTypes) {
391
+ const stateProperty = getStatePropertyForModifier(modifierType);
392
+ if (!usedStateProps.has(stateProperty)) {
393
+ usedStateProps.add(stateProperty);
394
+ paramProperties.push(
395
+ t.objectProperty(t.identifier(stateProperty), t.identifier(stateProperty), false, true),
396
+ );
397
+ }
398
+ }
399
+
400
+ const param = t.objectPattern(paramProperties);
401
+
402
+ // Create arrow function: ({ pressed }) => styleExpression
403
+ return t.arrowFunctionExpression([param], styleExpression);
404
+ }
405
+
242
406
  export default function reactNativeTailwindBabelPlugin({
243
407
  types: t,
244
408
  }: {
@@ -311,7 +475,7 @@ export default function reactNativeTailwindBabelPlugin({
311
475
  // Determine target style prop based on attribute name
312
476
  const targetStyleProp = getTargetStyleProp(attributeName);
313
477
 
314
- // Handle static string literals (original behavior)
478
+ // Handle static string literals
315
479
  if (t.isStringLiteral(value)) {
316
480
  const className = value.value.trim();
317
481
 
@@ -323,13 +487,99 @@ export default function reactNativeTailwindBabelPlugin({
323
487
 
324
488
  state.hasClassNames = true;
325
489
 
326
- // Parse className to React Native styles
327
- const styleObject = parseClassName(className, state.customColors);
490
+ // Check if className contains modifiers (active:, hover:, focus:)
491
+ const { baseClasses, modifierClasses } = splitModifierClasses(className);
328
492
 
329
- // Generate unique style key
330
- const styleKey = generateStyleKey(className);
493
+ // If there are modifiers, check if this component supports them
494
+ if (modifierClasses.length > 0) {
495
+ // Get the JSX opening element (the direct parent of the attribute)
496
+ const jsxOpeningElement = path.parent;
497
+ const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
498
+
499
+ if (componentSupport) {
500
+ // Get modifier types used in className
501
+ const usedModifiers = Array.from(new Set(modifierClasses.map((m) => m.modifier)));
502
+
503
+ // Check if all modifiers are supported by this component
504
+ const unsupportedModifiers = usedModifiers.filter(
505
+ (mod) => !componentSupport.supportedModifiers.includes(mod),
506
+ );
507
+
508
+ if (unsupportedModifiers.length > 0) {
509
+ // Warn about unsupported modifiers
510
+ if (process.env.NODE_ENV !== "production") {
511
+ console.warn(
512
+ `[react-native-tailwind] Modifiers (${unsupportedModifiers.map((m) => `${m}:`).join(", ")}) are not supported on ${componentSupport.component} component at ${state.file.opts.filename ?? "unknown"}. ` +
513
+ `Supported modifiers: ${componentSupport.supportedModifiers.join(", ")}`,
514
+ );
515
+ }
516
+ // Filter out unsupported modifiers
517
+ const supportedModifierClasses = modifierClasses.filter((m) =>
518
+ componentSupport.supportedModifiers.includes(m.modifier),
519
+ );
520
+
521
+ // If no supported modifiers remain, fall through to normal processing
522
+ if (supportedModifierClasses.length === 0) {
523
+ // Continue to normal processing
524
+ } else {
525
+ // Process only supported modifiers
526
+ const filteredClassName =
527
+ baseClasses.join(" ") +
528
+ " " +
529
+ supportedModifierClasses.map((m) => `${m.modifier}:${m.baseClass}`).join(" ");
530
+ const styleExpression = processStaticClassNameWithModifiers(
531
+ filteredClassName.trim(),
532
+ state,
533
+ t,
534
+ );
535
+ const modifierTypes = Array.from(new Set(supportedModifierClasses.map((m) => m.modifier)));
536
+ const styleFunctionExpression = createStyleFunction(styleExpression, modifierTypes, t);
537
+
538
+ const parent = path.parent as any;
539
+ const styleAttribute = parent.attributes.find(
540
+ (attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
541
+ );
542
+
543
+ if (styleAttribute) {
544
+ mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
545
+ } else {
546
+ replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
547
+ }
548
+ return;
549
+ }
550
+ } else {
551
+ // All modifiers are supported - process normally
552
+ const styleExpression = processStaticClassNameWithModifiers(className, state, t);
553
+ const modifierTypes = usedModifiers;
554
+ const styleFunctionExpression = createStyleFunction(styleExpression, modifierTypes, t);
555
+
556
+ const parent = path.parent as any;
557
+ const styleAttribute = parent.attributes.find(
558
+ (attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
559
+ );
560
+
561
+ if (styleAttribute) {
562
+ mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
563
+ } else {
564
+ replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
565
+ }
566
+ return;
567
+ }
568
+ } else {
569
+ // Component doesn't support any modifiers
570
+ if (process.env.NODE_ENV !== "production") {
571
+ const usedModifiers = Array.from(new Set(modifierClasses.map((m) => m.modifier)));
572
+ console.warn(
573
+ `[react-native-tailwind] Modifiers (${usedModifiers.map((m) => `${m}:`).join(", ")}) can only be used on compatible components (Pressable, TextInput). Found on unsupported element at ${state.file.opts.filename ?? "unknown"}`,
574
+ );
575
+ }
576
+ // Fall through to normal processing (ignore modifiers)
577
+ }
578
+ }
331
579
 
332
- // Store in registry
580
+ // Normal processing without modifiers
581
+ const styleObject = parseClassName(className, state.customColors);
582
+ const styleKey = generateStyleKey(className);
333
583
  state.styleRegistry.set(styleKey, styleObject);
334
584
 
335
585
  // Check if there's already a style prop on this element
@@ -426,7 +676,7 @@ function replaceWithStyleAttribute(
426
676
  ) {
427
677
  const styleAttribute = t.jsxAttribute(
428
678
  t.jsxIdentifier(targetStyleProp),
429
- t.jsxExpressionContainer(t.memberExpression(t.identifier("styles"), t.identifier(styleKey))),
679
+ t.jsxExpressionContainer(t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey))),
430
680
  );
431
681
 
432
682
  classNamePath.replaceWith(styleAttribute);
@@ -446,7 +696,7 @@ function mergeStyleAttribute(
446
696
  // Create array with className styles first, then existing styles
447
697
  // This allows existing styles to override className styles
448
698
  const styleArray = t.arrayExpression([
449
- t.memberExpression(t.identifier("styles"), t.identifier(styleKey)),
699
+ t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(styleKey)),
450
700
  existingStyle,
451
701
  ]);
452
702
 
@@ -502,13 +752,72 @@ function mergeDynamicStyleAttribute(
502
752
  }
503
753
 
504
754
  /**
505
- * Inject StyleSheet.create with all collected styles
755
+ * Replace className with style function attribute (for Pressable with modifiers)
506
756
  */
507
- function injectStyles(
508
- path: NodePath,
509
- styleRegistry: Map<string, Record<string, string | number>>,
757
+ function replaceWithStyleFunctionAttribute(
758
+ classNamePath: NodePath,
759
+ styleFunctionExpression: any,
760
+ targetStyleProp: string,
510
761
  t: typeof BabelTypes,
511
762
  ) {
763
+ const styleAttribute = t.jsxAttribute(
764
+ t.jsxIdentifier(targetStyleProp),
765
+ t.jsxExpressionContainer(styleFunctionExpression),
766
+ );
767
+
768
+ classNamePath.replaceWith(styleAttribute);
769
+ }
770
+
771
+ /**
772
+ * Merge className style function with existing style prop (for Pressable with modifiers)
773
+ */
774
+ function mergeStyleFunctionAttribute(
775
+ classNamePath: NodePath,
776
+ styleAttribute: any,
777
+ styleFunctionExpression: any,
778
+ t: typeof BabelTypes,
779
+ ) {
780
+ const existingStyle = styleAttribute.value.expression;
781
+
782
+ // Create a wrapper function that merges both styles
783
+ // ({ pressed }) => [styleFunctionResult, existingStyle]
784
+ // We need to call the style function and merge results
785
+
786
+ // If existing is already a function, we need to handle it specially
787
+ if (t.isArrowFunctionExpression(existingStyle) || t.isFunctionExpression(existingStyle)) {
788
+ // Both are functions - create wrapper that calls both
789
+ // (_state) => [newStyleFn(_state), existingStyleFn(_state)]
790
+ // Create an identifier for the parameter to pass to the function calls
791
+ const paramIdentifier = t.identifier("_state");
792
+
793
+ const newFunctionCall = t.callExpression(styleFunctionExpression, [paramIdentifier]);
794
+ const existingFunctionCall = t.callExpression(existingStyle, [paramIdentifier]);
795
+
796
+ const mergedArray = t.arrayExpression([newFunctionCall, existingFunctionCall]);
797
+ const wrapperFunction = t.arrowFunctionExpression([paramIdentifier], mergedArray);
798
+
799
+ styleAttribute.value = t.jsxExpressionContainer(wrapperFunction);
800
+ } else {
801
+ // Existing is static - create function that returns array
802
+ // (_state) => [styleFunctionResult, existingStyle]
803
+ // Create an identifier for the parameter to pass to the function call
804
+ const paramIdentifier = t.identifier("_state");
805
+
806
+ const functionCall = t.callExpression(styleFunctionExpression, [paramIdentifier]);
807
+ const mergedArray = t.arrayExpression([functionCall, existingStyle]);
808
+ const wrapperFunction = t.arrowFunctionExpression([paramIdentifier], mergedArray);
809
+
810
+ styleAttribute.value = t.jsxExpressionContainer(wrapperFunction);
811
+ }
812
+
813
+ // Remove the className attribute
814
+ classNamePath.remove();
815
+ }
816
+
817
+ /**
818
+ * Inject StyleSheet.create with all collected styles
819
+ */
820
+ function injectStyles(path: NodePath, styleRegistry: Map<string, StyleObject>, t: typeof BabelTypes) {
512
821
  // Build style object properties
513
822
  const styleProperties: any[] = [];
514
823
 
@@ -531,10 +840,10 @@ function injectStyles(
531
840
  styleProperties.push(t.objectProperty(t.identifier(key), t.objectExpression(properties)));
532
841
  }
533
842
 
534
- // Create: const styles = StyleSheet.create({ ... })
843
+ // Create: const _tailwindStyles = StyleSheet.create({ ... })
535
844
  const styleSheet = t.variableDeclaration("const", [
536
845
  t.variableDeclarator(
537
- t.identifier("styles"),
846
+ t.identifier(STYLES_IDENTIFIER),
538
847
  t.callExpression(t.memberExpression(t.identifier("StyleSheet"), t.identifier("create")), [
539
848
  t.objectExpression(styleProperties),
540
849
  ]),
@@ -546,10 +855,7 @@ function injectStyles(
546
855
  }
547
856
 
548
857
  // Helper functions that use the imported parser
549
- function parseClassName(
550
- className: string,
551
- customColors: Record<string, string>,
552
- ): Record<string, string | number> {
858
+ function parseClassName(className: string, customColors: Record<string, string>): StyleObject {
553
859
  return parseClassNameFn(className, customColors);
554
860
  }
555
861
 
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Enhanced Pressable component with modifier support
3
+ * Injects disabled state into style function for disabled: modifier support
4
+ */
5
+
6
+ import type { ComponentRef } from "react";
7
+ import { forwardRef } from "react";
8
+ import {
9
+ Pressable as RNPressable,
10
+ type PressableStateCallbackType,
11
+ type PressableProps as RNPressableProps,
12
+ type StyleProp,
13
+ type ViewStyle,
14
+ } from "react-native";
15
+
16
+ // Extend PressableStateCallbackType to include disabled
17
+ type EnhancedPressableState = PressableStateCallbackType & { disabled: boolean | null | undefined };
18
+
19
+ export type PressableProps = Omit<RNPressableProps, "style"> & {
20
+ /**
21
+ * Style can be a static style object/array or a function that receives Pressable state + disabled
22
+ */
23
+ style?: StyleProp<ViewStyle> | ((state: EnhancedPressableState) => StyleProp<ViewStyle>);
24
+ };
25
+
26
+ /**
27
+ * Enhanced Pressable that supports the disabled: modifier
28
+ *
29
+ * @example
30
+ * <Pressable
31
+ * disabled={isLoading}
32
+ * className="bg-blue-500 active:bg-blue-700 disabled:bg-gray-400"
33
+ * >
34
+ * <Text>Submit</Text>
35
+ * </Pressable>
36
+ */
37
+ export const Pressable = forwardRef<ComponentRef<typeof RNPressable>, PressableProps>(function Pressable(
38
+ { style, disabled = false, ...props },
39
+ ref,
40
+ ) {
41
+ // Inject disabled into style function context
42
+ const resolvedStyle =
43
+ typeof style === "function" ? (state: PressableStateCallbackType) => style({ ...state, disabled }) : style;
44
+
45
+ return <RNPressable ref={ref} disabled={disabled} style={resolvedStyle} {...props} />;
46
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Enhanced TextInput component with focus state support for focus: modifier
3
+ *
4
+ * This component wraps React Native's TextInput and manages focus state internally,
5
+ * allowing the style prop to be a function that receives { focused: boolean }.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { TextInput } from '@mgcrea/react-native-tailwind';
10
+ *
11
+ * <TextInput
12
+ * className="border-2 border-gray-300 focus:border-blue-500 p-3 rounded-lg"
13
+ * placeholder="Email"
14
+ * />
15
+ * ```
16
+ */
17
+
18
+ import { forwardRef, useCallback, useState } from "react";
19
+ import {
20
+ type BlurEvent,
21
+ type FocusEvent,
22
+ TextInput as RNTextInput,
23
+ type TextInputProps as RNTextInputProps,
24
+ } from "react-native";
25
+
26
+ export type TextInputProps = Omit<RNTextInputProps, "style"> & {
27
+ /**
28
+ * Style can be a static style object/array or a function that receives focus and disabled state
29
+ */
30
+ style?:
31
+ | RNTextInputProps["style"]
32
+ | ((state: { focused: boolean; disabled: boolean }) => RNTextInputProps["style"]);
33
+ /**
34
+ * Convenience prop for disabled state (overrides editable if provided)
35
+ * When true, sets editable to false
36
+ */
37
+ disabled?: boolean;
38
+ };
39
+
40
+ /**
41
+ * Enhanced TextInput with focus and disabled state support
42
+ *
43
+ * Manages focus state internally and passes it to style functions,
44
+ * enabling the use of focus: and disabled: modifiers in className.
45
+ *
46
+ * Note: TextInput uses `editable` prop internally. You can pass either:
47
+ * - `disabled={true}` - convenience prop (sets editable to false)
48
+ * - `editable={false}` - React Native's native prop
49
+ * If both are provided, `disabled` takes precedence.
50
+ */
51
+ export const TextInput = forwardRef<RNTextInput, TextInputProps>(function TextInput(
52
+ { style, onFocus, onBlur, disabled, editable = true, ...props },
53
+ ref,
54
+ ) {
55
+ const [focused, setFocused] = useState(false);
56
+
57
+ const handleFocus = useCallback(
58
+ (e: FocusEvent) => {
59
+ setFocused(true);
60
+ onFocus?.(e);
61
+ },
62
+ [onFocus],
63
+ );
64
+
65
+ const handleBlur = useCallback(
66
+ (e: BlurEvent) => {
67
+ setFocused(false);
68
+ onBlur?.(e);
69
+ },
70
+ [onBlur],
71
+ );
72
+
73
+ // Resolve editable state: disabled prop overrides editable if provided
74
+ const isEditable = disabled !== undefined ? !disabled : editable;
75
+ const isDisabled = !isEditable;
76
+
77
+ // Resolve style - call function with focus and disabled state if needed
78
+ const resolvedStyle = typeof style === "function" ? style({ focused, disabled: isDisabled }) : style;
79
+
80
+ return (
81
+ <RNTextInput
82
+ ref={ref}
83
+ style={resolvedStyle}
84
+ editable={isEditable}
85
+ onFocus={handleFocus}
86
+ onBlur={handleBlur}
87
+ {...props}
88
+ />
89
+ );
90
+ });