@mgcrea/react-native-tailwind 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +29 -2014
  2. package/dist/babel/index.cjs +1462 -1155
  3. package/dist/babel/plugin/componentScope.d.ts +26 -0
  4. package/dist/babel/plugin/componentScope.ts +87 -0
  5. package/dist/babel/plugin/state.d.ts +119 -0
  6. package/dist/babel/plugin/state.ts +177 -0
  7. package/dist/babel/plugin/visitors/className.d.ts +11 -0
  8. package/{src/babel/plugin.test.ts → dist/babel/plugin/visitors/className.test.ts} +74 -674
  9. package/dist/babel/plugin/visitors/className.ts +624 -0
  10. package/dist/babel/plugin/visitors/className.windowDimensions.test.ts +406 -0
  11. package/dist/babel/plugin/visitors/imports.d.ts +11 -0
  12. package/dist/babel/plugin/visitors/imports.test.ts +88 -0
  13. package/dist/babel/plugin/visitors/imports.ts +101 -0
  14. package/dist/babel/plugin/visitors/program.d.ts +15 -0
  15. package/dist/babel/plugin/visitors/program.test.ts +325 -0
  16. package/dist/babel/plugin/visitors/program.ts +99 -0
  17. package/dist/babel/plugin/visitors/tw.d.ts +16 -0
  18. package/dist/babel/plugin/visitors/tw.test.ts +620 -0
  19. package/dist/babel/plugin/visitors/tw.ts +148 -0
  20. package/dist/babel/plugin.d.ts +3 -96
  21. package/dist/babel/plugin.test.ts +470 -0
  22. package/dist/babel/plugin.ts +28 -963
  23. package/dist/babel/utils/colorSchemeModifierProcessing.ts +11 -0
  24. package/dist/babel/utils/componentSupport.test.ts +20 -7
  25. package/dist/babel/utils/componentSupport.ts +2 -0
  26. package/dist/babel/utils/modifierProcessing.ts +21 -0
  27. package/dist/babel/utils/platformModifierProcessing.ts +11 -0
  28. package/dist/babel/utils/styleInjection.d.ts +15 -0
  29. package/dist/babel/utils/styleInjection.ts +115 -0
  30. package/dist/babel/utils/twProcessing.ts +11 -0
  31. package/dist/babel/utils/windowDimensionsProcessing.d.ts +56 -0
  32. package/dist/babel/utils/windowDimensionsProcessing.ts +121 -0
  33. package/dist/components/TouchableOpacity.d.ts +35 -0
  34. package/dist/components/TouchableOpacity.js +1 -0
  35. package/dist/components/index.d.ts +3 -0
  36. package/dist/components/index.js +1 -0
  37. package/dist/config/markers.d.ts +5 -0
  38. package/dist/config/markers.js +1 -0
  39. package/dist/index.d.ts +2 -5
  40. package/dist/index.js +1 -1
  41. package/dist/parser/borders.d.ts +3 -1
  42. package/dist/parser/borders.js +1 -1
  43. package/dist/parser/borders.test.js +1 -1
  44. package/dist/parser/colors.js +1 -1
  45. package/dist/parser/colors.test.js +1 -1
  46. package/dist/parser/index.js +1 -1
  47. package/dist/parser/sizing.js +1 -1
  48. package/dist/runtime.cjs +1 -1
  49. package/dist/runtime.cjs.map +4 -4
  50. package/dist/runtime.js +1 -1
  51. package/dist/runtime.js.map +4 -4
  52. package/package.json +1 -1
  53. package/src/babel/plugin/componentScope.ts +87 -0
  54. package/src/babel/plugin/state.ts +177 -0
  55. package/src/babel/plugin/visitors/className.test.ts +1312 -0
  56. package/src/babel/plugin/visitors/className.ts +624 -0
  57. package/src/babel/plugin/visitors/className.windowDimensions.test.ts +406 -0
  58. package/src/babel/plugin/visitors/imports.test.ts +88 -0
  59. package/src/babel/plugin/visitors/imports.ts +101 -0
  60. package/src/babel/plugin/visitors/program.test.ts +325 -0
  61. package/src/babel/plugin/visitors/program.ts +99 -0
  62. package/src/babel/plugin/visitors/tw.test.ts +620 -0
  63. package/src/babel/plugin/visitors/tw.ts +148 -0
  64. package/src/babel/plugin.ts +28 -963
  65. package/src/babel/utils/colorSchemeModifierProcessing.ts +11 -0
  66. package/src/babel/utils/componentSupport.test.ts +20 -7
  67. package/src/babel/utils/componentSupport.ts +2 -0
  68. package/src/babel/utils/modifierProcessing.ts +21 -0
  69. package/src/babel/utils/platformModifierProcessing.ts +11 -0
  70. package/src/babel/utils/styleInjection.ts +115 -0
  71. package/src/babel/utils/twProcessing.ts +11 -0
  72. package/src/babel/utils/windowDimensionsProcessing.ts +121 -0
  73. package/src/components/TouchableOpacity.tsx +71 -0
  74. package/src/components/index.ts +3 -0
  75. package/src/config/markers.ts +5 -0
  76. package/src/index.ts +4 -5
  77. package/src/parser/borders.test.ts +58 -0
  78. package/src/parser/borders.ts +18 -3
  79. package/src/parser/colors.test.ts +249 -0
  80. package/src/parser/colors.ts +38 -0
  81. package/src/parser/index.ts +2 -2
  82. package/src/parser/sizing.ts +11 -0
@@ -5,6 +5,7 @@
5
5
  import type * as BabelTypes from "@babel/types";
6
6
  import type { ColorSchemeModifierType, CustomTheme, ParsedModifier } from "../../parser/index.js";
7
7
  import type { StyleObject } from "../../types/core.js";
8
+ import { hasRuntimeDimensions } from "./windowDimensionsProcessing.js";
8
9
 
9
10
  /**
10
11
  * Plugin state interface (subset needed for color scheme modifier processing)
@@ -66,6 +67,16 @@ export function processColorSchemeModifiers(
66
67
  // Parse all classes for this color scheme together
67
68
  const classNames = modifiers.map((m) => m.baseClass).join(" ");
68
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 color scheme modifiers (dark:, light:, scheme:). ` +
75
+ `Found in: "${scheme}:${classNames}". ` +
76
+ `Use w-screen/h-screen without modifiers instead.`,
77
+ );
78
+ }
79
+
69
80
  const styleKey = generateStyleKey(`${scheme}_${classNames}`);
70
81
 
71
82
  // Register style in the registry
@@ -86,6 +86,26 @@ describe("getComponentModifierSupport", () => {
86
86
  supportedModifiers: ["focus", "disabled", "placeholder"],
87
87
  });
88
88
  });
89
+
90
+ it("should recognize TouchableOpacity component", () => {
91
+ const element = createJSXElement("<TouchableOpacity />");
92
+ const result = getComponentModifierSupport(element, t);
93
+
94
+ expect(result).toEqual({
95
+ component: "TouchableOpacity",
96
+ supportedModifiers: ["active", "disabled"],
97
+ });
98
+ });
99
+
100
+ it("should recognize TouchableOpacity with attributes", () => {
101
+ const element = createJSXElement('<TouchableOpacity className="m-4" onPress={handlePress} />');
102
+ const result = getComponentModifierSupport(element, t);
103
+
104
+ expect(result).toEqual({
105
+ component: "TouchableOpacity",
106
+ supportedModifiers: ["active", "disabled"],
107
+ });
108
+ });
89
109
  });
90
110
 
91
111
  describe("Member expressions", () => {
@@ -139,13 +159,6 @@ describe("getComponentModifierSupport", () => {
139
159
  expect(result).toBeNull();
140
160
  });
141
161
 
142
- it("should return null for TouchableOpacity", () => {
143
- const element = createJSXElement("<TouchableOpacity />");
144
- const result = getComponentModifierSupport(element, t);
145
-
146
- expect(result).toBeNull();
147
- });
148
-
149
162
  it("should return null for custom components", () => {
150
163
  const element = createJSXElement("<CustomButton />");
151
164
  const result = getComponentModifierSupport(element, t);
@@ -41,6 +41,8 @@ export function getComponentModifierSupport(
41
41
  switch (componentName) {
42
42
  case "Pressable":
43
43
  return { component: "Pressable", supportedModifiers: ["active", "hover", "focus", "disabled"] };
44
+ case "TouchableOpacity":
45
+ return { component: "TouchableOpacity", supportedModifiers: ["active", "disabled"] };
44
46
  case "TextInput":
45
47
  return { component: "TextInput", supportedModifiers: ["focus", "disabled", "placeholder"] };
46
48
  default:
@@ -6,6 +6,7 @@ import type * as BabelTypes from "@babel/types";
6
6
  import type { CustomTheme, ModifierType, ParsedModifier } from "../../parser/index.js";
7
7
  import type { StyleObject } from "../../types/core.js";
8
8
  import { getStatePropertyForModifier } from "./componentSupport.js";
9
+ import { hasRuntimeDimensions } from "./windowDimensionsProcessing.js";
9
10
 
10
11
  /**
11
12
  * Plugin state interface (subset needed for modifier processing)
@@ -36,6 +37,16 @@ export function processStaticClassNameWithModifiers(
36
37
  if (baseClasses.length > 0) {
37
38
  const baseClassName = baseClasses.join(" ");
38
39
  const baseStyleObject = parseClassName(baseClassName, state.customTheme);
40
+
41
+ // Check for runtime dimensions (w-screen, h-screen) in base classes
42
+ if (hasRuntimeDimensions(baseStyleObject)) {
43
+ throw new Error(
44
+ `w-screen and h-screen cannot be combined with state modifiers (active:, hover:, focus:, etc.) or platform modifiers (ios:, android:, web:). ` +
45
+ `Found in: "${baseClassName}". ` +
46
+ `Use w-screen/h-screen without modifiers instead.`,
47
+ );
48
+ }
49
+
39
50
  const baseStyleKey = generateStyleKey(baseClassName);
40
51
  state.styleRegistry.set(baseStyleKey, baseStyleObject);
41
52
  baseStyleExpression = t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey));
@@ -67,6 +78,16 @@ export function processStaticClassNameWithModifiers(
67
78
  // Parse all modifier classes together
68
79
  const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
69
80
  const modifierStyleObject = parseClassName(modifierClassNames, state.customTheme);
81
+
82
+ // Check for runtime dimensions (w-screen, h-screen) in modifier classes
83
+ if (hasRuntimeDimensions(modifierStyleObject)) {
84
+ throw new Error(
85
+ `w-screen and h-screen cannot be combined with state modifiers (active:, hover:, focus:, etc.) or platform modifiers (ios:, android:, web:). ` +
86
+ `Found in: "${modifierType}:${modifierClassNames}". ` +
87
+ `Use w-screen/h-screen without modifiers instead.`,
88
+ );
89
+ }
90
+
70
91
  const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
71
92
  state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
72
93
 
@@ -5,6 +5,7 @@
5
5
  import type * as BabelTypes from "@babel/types";
6
6
  import type { CustomTheme, ParsedModifier, PlatformModifierType } from "../../parser/index.js";
7
7
  import type { StyleObject } from "../../types/core.js";
8
+ import { hasRuntimeDimensions } from "./windowDimensionsProcessing.js";
8
9
 
9
10
  /**
10
11
  * Plugin state interface (subset needed for platform modifier processing)
@@ -62,6 +63,16 @@ export function processPlatformModifiers(
62
63
  // Parse all classes for this platform together
63
64
  const classNames = modifiers.map((m) => m.baseClass).join(" ");
64
65
  const styleObject = parseClassName(classNames, state.customTheme);
66
+
67
+ // Check for runtime dimensions (w-screen, h-screen)
68
+ if (hasRuntimeDimensions(styleObject)) {
69
+ throw new Error(
70
+ `w-screen and h-screen cannot be combined with platform modifiers (ios:, android:, web:). ` +
71
+ `Found in: "${platform}:${classNames}". ` +
72
+ `Use w-screen/h-screen without modifiers instead.`,
73
+ );
74
+ }
75
+
65
76
  const styleKey = generateStyleKey(`${platform}_${classNames}`);
66
77
 
67
78
  // Register style in the registry
@@ -220,6 +220,121 @@ export function injectColorSchemeHook(
220
220
  return true;
221
221
  }
222
222
 
223
+ /**
224
+ * Add useWindowDimensions import to the file or merge with existing react-native import
225
+ */
226
+ export function addWindowDimensionsImport(path: NodePath<BabelTypes.Program>, t: typeof BabelTypes): void {
227
+ // Check if there's already an 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
+ // Only consider value imports (not type-only imports which get erased)
234
+ if (statement.importKind !== "type") {
235
+ existingValueImport = statement;
236
+ break; // Found a value import, we can stop
237
+ }
238
+ }
239
+ }
240
+
241
+ // If we found a value import (not type-only), merge with it
242
+ if (existingValueImport) {
243
+ // Check if the hook is already imported
244
+ const hasHook = existingValueImport.specifiers.some(
245
+ (spec) =>
246
+ t.isImportSpecifier(spec) &&
247
+ spec.imported.type === "Identifier" &&
248
+ spec.imported.name === "useWindowDimensions",
249
+ );
250
+
251
+ if (!hasHook) {
252
+ // Add hook to existing value import
253
+ existingValueImport.specifiers.push(
254
+ t.importSpecifier(t.identifier("useWindowDimensions"), t.identifier("useWindowDimensions")),
255
+ );
256
+ }
257
+ } else {
258
+ // No value import exists - create a new one
259
+ // (Don't merge with type-only imports as they get erased by Babel/TypeScript)
260
+ const importDeclaration = t.importDeclaration(
261
+ [t.importSpecifier(t.identifier("useWindowDimensions"), t.identifier("useWindowDimensions"))],
262
+ t.stringLiteral("react-native"),
263
+ );
264
+ path.unshiftContainer("body", importDeclaration);
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Inject useWindowDimensions hook call at the top of a function component
270
+ *
271
+ * @param functionPath - Path to the function component
272
+ * @param dimensionsVariableName - Name for the dimensions variable
273
+ * @param hookName - Name of the hook to call (e.g., 'useWindowDimensions')
274
+ * @param localIdentifier - Local identifier if hook is already imported with an alias
275
+ * @param t - Babel types
276
+ * @returns true if hook was injected, false if already exists
277
+ */
278
+ export function injectWindowDimensionsHook(
279
+ functionPath: NodePath<BabelTypes.Function>,
280
+ dimensionsVariableName: string,
281
+ hookName: string,
282
+ localIdentifier: string | undefined,
283
+ t: typeof BabelTypes,
284
+ ): boolean {
285
+ let body = functionPath.node.body;
286
+
287
+ // Handle concise arrow functions: () => <JSX />
288
+ // Convert to block statement: () => { const _twDimensions = useWindowDimensions(); return <JSX />; }
289
+ if (!t.isBlockStatement(body)) {
290
+ if (t.isArrowFunctionExpression(functionPath.node) && t.isExpression(body)) {
291
+ // Convert concise body to block statement with return
292
+ const returnStatement = t.returnStatement(body);
293
+ const blockStatement = t.blockStatement([returnStatement]);
294
+ functionPath.node.body = blockStatement;
295
+ body = blockStatement;
296
+ } else {
297
+ // Other non-block functions (shouldn't happen for components, but be safe)
298
+ return false;
299
+ }
300
+ }
301
+
302
+ // Check if hook is already injected
303
+ const hasHook = body.body.some((statement) => {
304
+ if (
305
+ t.isVariableDeclaration(statement) &&
306
+ statement.declarations.length > 0 &&
307
+ t.isVariableDeclarator(statement.declarations[0])
308
+ ) {
309
+ const declarator = statement.declarations[0];
310
+ return t.isIdentifier(declarator.id) && declarator.id.name === dimensionsVariableName;
311
+ }
312
+ return false;
313
+ });
314
+
315
+ if (hasHook) {
316
+ return false; // Already injected
317
+ }
318
+
319
+ // Use the local identifier if hook was already imported with an alias,
320
+ // otherwise use the configured hook name
321
+ // e.g., import { useWindowDimensions as useDims } → call useDims()
322
+ const identifierToCall = localIdentifier ?? hookName;
323
+
324
+ // Create: const _twDimensions = useWindowDimensions(); (or aliased name if already imported)
325
+ const hookCall = t.variableDeclaration("const", [
326
+ t.variableDeclarator(
327
+ t.identifier(dimensionsVariableName),
328
+ t.callExpression(t.identifier(identifierToCall), []),
329
+ ),
330
+ ]);
331
+
332
+ // Insert at the beginning of function body
333
+ body.body.unshift(hookCall);
334
+
335
+ return true;
336
+ }
337
+
223
338
  /**
224
339
  * Inject StyleSheet.create with all collected styles at the top of the file
225
340
  * This ensures the styles object is defined before any code that references it
@@ -15,6 +15,7 @@ import type { SchemeModifierConfig } from "../../types/config.js";
15
15
  import type { StyleObject } from "../../types/core.js";
16
16
  import { processColorSchemeModifiers } from "./colorSchemeModifierProcessing.js";
17
17
  import { processPlatformModifiers } from "./platformModifierProcessing.js";
18
+ import { hasRuntimeDimensions } from "./windowDimensionsProcessing.js";
18
19
 
19
20
  /**
20
21
  * Plugin state interface (subset needed for tw processing)
@@ -77,6 +78,16 @@ export function processTwCall(
77
78
  if (baseClasses.length > 0) {
78
79
  const baseClassName = baseClasses.join(" ");
79
80
  const baseStyleObject = parseClassName(baseClassName, state.customTheme);
81
+
82
+ // Check for runtime dimensions (w-screen, h-screen)
83
+ if (hasRuntimeDimensions(baseStyleObject)) {
84
+ throw path.buildCodeFrameError(
85
+ `w-screen and h-screen are not supported in tw\`\` or twStyle() calls. ` +
86
+ `Found: "${baseClassName}". ` +
87
+ `Use them in className attributes instead.`,
88
+ );
89
+ }
90
+
80
91
  const baseStyleKey = generateStyleKey(baseClassName);
81
92
  state.styleRegistry.set(baseStyleKey, baseStyleObject);
82
93
 
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Utility functions for processing window dimensions (w-screen, h-screen)
3
+ */
4
+
5
+ import type * as BabelTypes from "@babel/types";
6
+ import { RUNTIME_DIMENSIONS_MARKER } from "../../config/markers.js";
7
+ import type { StyleObject } from "../../types/core.js";
8
+
9
+ /**
10
+ * Plugin state interface (subset needed for window dimensions processing)
11
+ */
12
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
13
+ export interface WindowDimensionsProcessingState {
14
+ needsWindowDimensionsImport: boolean;
15
+ windowDimensionsVariableName: string;
16
+ }
17
+
18
+ /**
19
+ * Check if a style object contains runtime dimension markers
20
+ *
21
+ * @param styleObject - Style object to check
22
+ * @returns true if the style object contains runtime dimension markers
23
+ *
24
+ * @example
25
+ * hasRuntimeDimensions({ width: "{{RUNTIME:dimensions.width}}" }) // true
26
+ * hasRuntimeDimensions({ width: 100 }) // false
27
+ */
28
+ export function hasRuntimeDimensions(styleObject: StyleObject): boolean {
29
+ return Object.values(styleObject).some(
30
+ (value) => typeof value === "string" && value.startsWith(RUNTIME_DIMENSIONS_MARKER),
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Create an inline style object with runtime dimension access
36
+ *
37
+ * Converts runtime markers like "{{RUNTIME:dimensions.width}}" to
38
+ * AST nodes like: { width: _twDimensions.width }
39
+ *
40
+ * @param styleObject - Style object with runtime markers
41
+ * @param state - Plugin state
42
+ * @param t - Babel types
43
+ * @returns AST object expression for inline style
44
+ *
45
+ * @example
46
+ * Input: { width: "{{RUNTIME:dimensions.width}}", height: "{{RUNTIME:dimensions.height}}" }
47
+ * Output: { width: _twDimensions.width, height: _twDimensions.height }
48
+ */
49
+ export function createRuntimeDimensionObject(
50
+ styleObject: StyleObject,
51
+ state: WindowDimensionsProcessingState,
52
+ t: typeof BabelTypes,
53
+ ): BabelTypes.ObjectExpression {
54
+ // Mark that we need useWindowDimensions import and hook injection
55
+ state.needsWindowDimensionsImport = true;
56
+
57
+ const properties: BabelTypes.ObjectProperty[] = [];
58
+
59
+ for (const [key, value] of Object.entries(styleObject)) {
60
+ let valueNode: BabelTypes.Expression;
61
+
62
+ if (typeof value === "string" && value.startsWith(RUNTIME_DIMENSIONS_MARKER)) {
63
+ // Extract property name: "{{RUNTIME:dimensions.width}}" -> "width"
64
+ const match = value.match(/dimensions\.(\w+)/);
65
+ const prop = match?.[1];
66
+
67
+ if (prop) {
68
+ // Generate: _twDimensions.width or _twDimensions.height
69
+ valueNode = t.memberExpression(t.identifier(state.windowDimensionsVariableName), t.identifier(prop));
70
+ } else {
71
+ // Fallback: shouldn't happen, but handle gracefully
72
+ valueNode = t.stringLiteral(value);
73
+ }
74
+ } else if (typeof value === "number") {
75
+ valueNode = t.numericLiteral(value);
76
+ } else if (typeof value === "string") {
77
+ valueNode = t.stringLiteral(value);
78
+ } else if (typeof value === "object" && value !== null) {
79
+ // Handle nested objects (e.g., transform arrays)
80
+ valueNode = t.valueToNode(value);
81
+ } else {
82
+ // Handle other types
83
+ valueNode = t.valueToNode(value);
84
+ }
85
+
86
+ properties.push(t.objectProperty(t.identifier(key), valueNode));
87
+ }
88
+
89
+ return t.objectExpression(properties);
90
+ }
91
+
92
+ /**
93
+ * Split a style object into static and runtime parts
94
+ *
95
+ * @param styleObject - Style object to split
96
+ * @returns Object with static and runtime style objects
97
+ *
98
+ * @example
99
+ * Input: { width: "{{RUNTIME:dimensions.width}}", padding: 16, backgroundColor: "#fff" }
100
+ * Output: {
101
+ * static: { padding: 16, backgroundColor: "#fff" },
102
+ * runtime: { width: "{{RUNTIME:dimensions.width}}" }
103
+ * }
104
+ */
105
+ export function splitStaticAndRuntimeStyles(styleObject: StyleObject): {
106
+ static: StyleObject;
107
+ runtime: StyleObject;
108
+ } {
109
+ const staticStyles: StyleObject = {};
110
+ const runtimeStyles: StyleObject = {};
111
+
112
+ for (const [key, value] of Object.entries(styleObject)) {
113
+ if (typeof value === "string" && value.startsWith(RUNTIME_DIMENSIONS_MARKER)) {
114
+ runtimeStyles[key] = value;
115
+ } else {
116
+ staticStyles[key] = value;
117
+ }
118
+ }
119
+
120
+ return { static: staticStyles, runtime: runtimeStyles };
121
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Enhanced TouchableOpacity component with modifier support
3
+ * Adds active state support for active: modifier via onPressIn/onPressOut
4
+ */
5
+
6
+ import type { ComponentRef } from "react";
7
+ import { forwardRef, useCallback, useState } from "react";
8
+ import {
9
+ TouchableOpacity as RNTouchableOpacity,
10
+ type TouchableOpacityProps as RNTouchableOpacityProps,
11
+ type StyleProp,
12
+ type ViewStyle,
13
+ } from "react-native";
14
+
15
+ // TouchableOpacity state for style function
16
+ type TouchableOpacityState = { active: boolean; disabled: boolean | null | undefined };
17
+
18
+ export type TouchableOpacityProps = Omit<RNTouchableOpacityProps, "style"> & {
19
+ /**
20
+ * Style can be a static style object/array or a function that receives TouchableOpacity state
21
+ */
22
+ style?: StyleProp<ViewStyle> | ((state: TouchableOpacityState) => StyleProp<ViewStyle>);
23
+ className?: string; // compile-time only
24
+ };
25
+
26
+ /**
27
+ * Enhanced TouchableOpacity that supports active: and disabled: modifiers
28
+ *
29
+ * @example
30
+ * <TouchableOpacity
31
+ * disabled={isLoading}
32
+ * className="bg-blue-500 active:bg-blue-700 disabled:bg-gray-400"
33
+ * >
34
+ * <Text>Submit</Text>
35
+ * </TouchableOpacity>
36
+ */
37
+ export const TouchableOpacity = forwardRef<ComponentRef<typeof RNTouchableOpacity>, TouchableOpacityProps>(
38
+ function TouchableOpacity({ style, disabled = false, onPressIn, onPressOut, ...props }, ref) {
39
+ const [isActive, setIsActive] = useState(false);
40
+
41
+ const handlePressIn = useCallback(
42
+ (event: Parameters<NonNullable<RNTouchableOpacityProps["onPressIn"]>>[0]) => {
43
+ setIsActive(true);
44
+ onPressIn?.(event);
45
+ },
46
+ [onPressIn],
47
+ );
48
+
49
+ const handlePressOut = useCallback(
50
+ (event: Parameters<NonNullable<RNTouchableOpacityProps["onPressOut"]>>[0]) => {
51
+ setIsActive(false);
52
+ onPressOut?.(event);
53
+ },
54
+ [onPressOut],
55
+ );
56
+
57
+ // Inject active and disabled state into style function context
58
+ const resolvedStyle = typeof style === "function" ? style({ active: isActive, disabled }) : style;
59
+
60
+ return (
61
+ <RNTouchableOpacity
62
+ ref={ref}
63
+ disabled={disabled}
64
+ style={resolvedStyle}
65
+ onPressIn={handlePressIn}
66
+ onPressOut={handlePressOut}
67
+ {...props}
68
+ />
69
+ );
70
+ },
71
+ );
@@ -0,0 +1,3 @@
1
+ export * from "./Pressable";
2
+ export * from "./TextInput";
3
+ export * from "./TouchableOpacity";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Runtime marker prefix for window dimension values
3
+ * Used to mark style values that need runtime evaluation via useWindowDimensions()
4
+ */
5
+ export const RUNTIME_DIMENSIONS_MARKER = "{{RUNTIME:dimensions.";
package/src/index.ts CHANGED
@@ -15,6 +15,9 @@ export { generateStyleKey } from "./utils/styleKey";
15
15
  export type { StyleObject } from "./types/core";
16
16
  export type { NativeStyle, TwStyle } from "./types/runtime";
17
17
 
18
+ // Re-export colors
19
+ export { TAILWIND_COLORS } from "./config/tailwind";
20
+
18
21
  // Re-export individual parsers for advanced usage
19
22
  export {
20
23
  parseAspectRatio,
@@ -39,8 +42,4 @@ export { SPACING_SCALE } from "./parser/spacing";
39
42
  export { FONT_SIZES, LETTER_SPACING_SCALE } from "./parser/typography";
40
43
 
41
44
  // Re-export enhanced components with modifier support
42
- export { Pressable } from "./components/Pressable";
43
- export type { PressableProps } from "./components/Pressable";
44
- export { TextInput } from "./components/TextInput";
45
- export type { TextInputProps } from "./components/TextInput";
46
- export { TAILWIND_COLORS } from "./config/tailwind";
45
+ export * from "./components";
@@ -327,3 +327,61 @@ describe("parseBorder - comprehensive coverage", () => {
327
327
  });
328
328
  });
329
329
  });
330
+
331
+ describe("parseBorder - color pattern detection", () => {
332
+ it("should return null for directional border colors with preset values", () => {
333
+ // These should be handled by parseColor
334
+ expect(parseBorder("border-t-red-500")).toBeNull();
335
+ expect(parseBorder("border-r-blue-500")).toBeNull();
336
+ expect(parseBorder("border-b-green-500")).toBeNull();
337
+ expect(parseBorder("border-l-yellow-500")).toBeNull();
338
+ });
339
+
340
+ it("should return null for directional border colors with basic values", () => {
341
+ // These should be handled by parseColor
342
+ expect(parseBorder("border-t-white")).toBeNull();
343
+ expect(parseBorder("border-r-black")).toBeNull();
344
+ expect(parseBorder("border-b-transparent")).toBeNull();
345
+ expect(parseBorder("border-l-white")).toBeNull();
346
+ });
347
+
348
+ it("should return null for directional border colors with arbitrary hex values", () => {
349
+ // These should be handled by parseColor
350
+ expect(parseBorder("border-t-[#ff0000]")).toBeNull();
351
+ expect(parseBorder("border-r-[#3B82F6]")).toBeNull();
352
+ expect(parseBorder("border-b-[#abc]")).toBeNull();
353
+ expect(parseBorder("border-l-[#00FF00AA]")).toBeNull();
354
+ });
355
+
356
+ it("should return null for directional border colors with opacity", () => {
357
+ // These should be handled by parseColor
358
+ expect(parseBorder("border-t-red-500/50")).toBeNull();
359
+ expect(parseBorder("border-r-blue-500/80")).toBeNull();
360
+ expect(parseBorder("border-b-[#ff0000]/60")).toBeNull();
361
+ expect(parseBorder("border-l-black/25")).toBeNull();
362
+ });
363
+
364
+ it("should return null for directional border colors with custom colors", () => {
365
+ // These should be handled by parseColor (assuming brand-primary is a custom color)
366
+ expect(parseBorder("border-t-brand-primary")).toBeNull();
367
+ expect(parseBorder("border-r-accent")).toBeNull();
368
+ expect(parseBorder("border-b-brand-secondary")).toBeNull();
369
+ expect(parseBorder("border-l-custom")).toBeNull();
370
+ });
371
+
372
+ it("should still handle directional border widths correctly", () => {
373
+ // These should NOT be detected as color patterns
374
+ expect(parseBorder("border-t-2")).toEqual({ borderTopWidth: 2 });
375
+ expect(parseBorder("border-r-4")).toEqual({ borderRightWidth: 4 });
376
+ expect(parseBorder("border-b-8")).toEqual({ borderBottomWidth: 8 });
377
+ expect(parseBorder("border-l-0")).toEqual({ borderLeftWidth: 0 });
378
+ });
379
+
380
+ it("should still handle directional border width arbitrary values", () => {
381
+ // These should NOT be detected as color patterns
382
+ expect(parseBorder("border-t-[3px]")).toEqual({ borderTopWidth: 3 });
383
+ expect(parseBorder("border-r-[5px]")).toEqual({ borderRightWidth: 5 });
384
+ expect(parseBorder("border-b-[10]")).toEqual({ borderBottomWidth: 10 });
385
+ expect(parseBorder("border-l-[8px]")).toEqual({ borderLeftWidth: 8 });
386
+ });
387
+ });
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import type { StyleObject } from "../types";
6
+ import { parseColor } from "./colors";
6
7
 
7
8
  // Border width scale
8
9
  export const BORDER_WIDTH_SCALE: Record<string, number> = {
@@ -108,8 +109,10 @@ function parseArbitraryBorderRadius(value: string): number | null {
108
109
 
109
110
  /**
110
111
  * Parse border classes
112
+ * @param cls - The class name to parse
113
+ * @param customColors - Optional custom colors from tailwind.config (used to detect color patterns)
111
114
  */
112
- export function parseBorder(cls: string): StyleObject | null {
115
+ export function parseBorder(cls: string, customColors?: Record<string, string>): StyleObject | null {
113
116
  // Border style (must come before parseBorderWidth)
114
117
  if (cls === "border-solid") return { borderStyle: "solid" };
115
118
  if (cls === "border-dotted") return { borderStyle: "dotted" };
@@ -117,7 +120,7 @@ export function parseBorder(cls: string): StyleObject | null {
117
120
 
118
121
  // Border width (border-0, border-t, border-[8px], etc.)
119
122
  if (cls.startsWith("border-")) {
120
- return parseBorderWidth(cls);
123
+ return parseBorderWidth(cls, customColors);
121
124
  }
122
125
 
123
126
  if (cls === "border") {
@@ -134,14 +137,26 @@ export function parseBorder(cls: string): StyleObject | null {
134
137
 
135
138
  /**
136
139
  * Parse border width classes
140
+ * @param cls - The class name to parse
141
+ * @param customColors - Optional custom colors (passed to parseColor for pattern detection)
137
142
  */
138
- function parseBorderWidth(cls: string): StyleObject | null {
143
+ function parseBorderWidth(cls: string, customColors?: Record<string, string>): StyleObject | null {
139
144
  // Directional borders: border-t, border-t-2, border-t-[8px]
145
+ // Note: border-x and border-y are handled by parseColor for colors only
140
146
  const dirMatch = cls.match(/^border-([trbl])(?:-(.+))?$/);
141
147
  if (dirMatch) {
142
148
  const dir = dirMatch[1];
143
149
  const valueStr = dirMatch[2] || ""; // empty string for border-t
144
150
 
151
+ // If it's a color pattern, let parseColor handle it
152
+ // Try to parse as color - if it succeeds, return null (let parseColor handle it)
153
+ if (valueStr) {
154
+ const colorResult = parseColor(cls, customColors);
155
+ if (colorResult !== null) {
156
+ return null; // It's a color, let parseColor handle it
157
+ }
158
+ }
159
+
145
160
  // Try arbitrary value first (if it starts with [)
146
161
  if (valueStr.startsWith("[")) {
147
162
  const arbitraryValue = parseArbitraryBorderWidth(valueStr);