@mgcrea/react-native-tailwind 0.12.1 → 0.14.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 (104) hide show
  1. package/README.md +45 -2031
  2. package/dist/babel/index.cjs +1726 -1094
  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 +123 -0
  6. package/dist/babel/plugin/state.ts +185 -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} +285 -572
  9. package/dist/babel/plugin/visitors/className.ts +652 -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 +116 -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 +116 -0
  17. package/dist/babel/plugin/visitors/tw.d.ts +16 -0
  18. package/dist/babel/plugin/visitors/tw.test.ts +771 -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/directionalModifierProcessing.d.ts +34 -0
  27. package/dist/babel/utils/directionalModifierProcessing.ts +99 -0
  28. package/dist/babel/utils/modifierProcessing.ts +21 -0
  29. package/dist/babel/utils/platformModifierProcessing.ts +11 -0
  30. package/dist/babel/utils/styleInjection.d.ts +31 -0
  31. package/dist/babel/utils/styleInjection.ts +253 -7
  32. package/dist/babel/utils/twProcessing.d.ts +2 -0
  33. package/dist/babel/utils/twProcessing.ts +103 -3
  34. package/dist/babel/utils/windowDimensionsProcessing.d.ts +56 -0
  35. package/dist/babel/utils/windowDimensionsProcessing.ts +121 -0
  36. package/dist/components/TouchableOpacity.d.ts +35 -0
  37. package/dist/components/TouchableOpacity.js +1 -0
  38. package/dist/components/index.d.ts +3 -0
  39. package/dist/components/index.js +1 -0
  40. package/dist/config/markers.d.ts +5 -0
  41. package/dist/config/markers.js +1 -0
  42. package/dist/index.d.ts +2 -5
  43. package/dist/index.js +1 -1
  44. package/dist/parser/borders.d.ts +3 -1
  45. package/dist/parser/borders.js +1 -1
  46. package/dist/parser/borders.test.js +1 -1
  47. package/dist/parser/colors.js +1 -1
  48. package/dist/parser/colors.test.js +1 -1
  49. package/dist/parser/index.d.ts +2 -2
  50. package/dist/parser/index.js +1 -1
  51. package/dist/parser/layout.js +1 -1
  52. package/dist/parser/layout.test.js +1 -1
  53. package/dist/parser/modifiers.d.ts +32 -2
  54. package/dist/parser/modifiers.js +1 -1
  55. package/dist/parser/modifiers.test.js +1 -1
  56. package/dist/parser/sizing.js +1 -1
  57. package/dist/parser/spacing.d.ts +1 -1
  58. package/dist/parser/spacing.js +1 -1
  59. package/dist/parser/spacing.test.js +1 -1
  60. package/dist/parser/typography.test.js +1 -1
  61. package/dist/runtime.cjs +1 -1
  62. package/dist/runtime.cjs.map +4 -4
  63. package/dist/runtime.js +1 -1
  64. package/dist/runtime.js.map +4 -4
  65. package/package.json +6 -6
  66. package/src/babel/plugin/componentScope.ts +87 -0
  67. package/src/babel/plugin/state.ts +185 -0
  68. package/src/babel/plugin/visitors/className.test.ts +1625 -0
  69. package/src/babel/plugin/visitors/className.ts +652 -0
  70. package/src/babel/plugin/visitors/className.windowDimensions.test.ts +406 -0
  71. package/src/babel/plugin/visitors/imports.test.ts +88 -0
  72. package/src/babel/plugin/visitors/imports.ts +116 -0
  73. package/src/babel/plugin/visitors/program.test.ts +325 -0
  74. package/src/babel/plugin/visitors/program.ts +116 -0
  75. package/src/babel/plugin/visitors/tw.test.ts +771 -0
  76. package/src/babel/plugin/visitors/tw.ts +148 -0
  77. package/src/babel/plugin.ts +28 -963
  78. package/src/babel/utils/colorSchemeModifierProcessing.ts +11 -0
  79. package/src/babel/utils/componentSupport.test.ts +20 -7
  80. package/src/babel/utils/componentSupport.ts +2 -0
  81. package/src/babel/utils/directionalModifierProcessing.ts +99 -0
  82. package/src/babel/utils/modifierProcessing.ts +21 -0
  83. package/src/babel/utils/platformModifierProcessing.ts +11 -0
  84. package/src/babel/utils/styleInjection.ts +253 -7
  85. package/src/babel/utils/twProcessing.ts +103 -3
  86. package/src/babel/utils/windowDimensionsProcessing.ts +121 -0
  87. package/src/components/TouchableOpacity.tsx +71 -0
  88. package/src/components/index.ts +3 -0
  89. package/src/config/markers.ts +5 -0
  90. package/src/index.ts +4 -5
  91. package/src/parser/borders.test.ts +162 -0
  92. package/src/parser/borders.ts +67 -9
  93. package/src/parser/colors.test.ts +249 -0
  94. package/src/parser/colors.ts +38 -0
  95. package/src/parser/index.ts +4 -2
  96. package/src/parser/layout.test.ts +74 -0
  97. package/src/parser/layout.ts +94 -0
  98. package/src/parser/modifiers.test.ts +206 -0
  99. package/src/parser/modifiers.ts +62 -3
  100. package/src/parser/sizing.ts +11 -0
  101. package/src/parser/spacing.test.ts +66 -0
  102. package/src/parser/spacing.ts +15 -5
  103. package/src/parser/typography.test.ts +8 -0
  104. package/src/parser/typography.ts +4 -0
@@ -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,165 @@ 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
+ });
388
+
389
+ describe("parseBorder - logical border width (RTL-aware)", () => {
390
+ it("should parse border start width", () => {
391
+ expect(parseBorder("border-s")).toEqual({ borderStartWidth: 1 });
392
+ expect(parseBorder("border-s-0")).toEqual({ borderStartWidth: 0 });
393
+ expect(parseBorder("border-s-2")).toEqual({ borderStartWidth: 2 });
394
+ expect(parseBorder("border-s-4")).toEqual({ borderStartWidth: 4 });
395
+ expect(parseBorder("border-s-8")).toEqual({ borderStartWidth: 8 });
396
+ });
397
+
398
+ it("should parse border end width", () => {
399
+ expect(parseBorder("border-e")).toEqual({ borderEndWidth: 1 });
400
+ expect(parseBorder("border-e-0")).toEqual({ borderEndWidth: 0 });
401
+ expect(parseBorder("border-e-2")).toEqual({ borderEndWidth: 2 });
402
+ expect(parseBorder("border-e-4")).toEqual({ borderEndWidth: 4 });
403
+ expect(parseBorder("border-e-8")).toEqual({ borderEndWidth: 8 });
404
+ });
405
+
406
+ it("should parse border start/end with arbitrary values", () => {
407
+ expect(parseBorder("border-s-[3px]")).toEqual({ borderStartWidth: 3 });
408
+ expect(parseBorder("border-s-[5]")).toEqual({ borderStartWidth: 5 });
409
+ expect(parseBorder("border-e-[3px]")).toEqual({ borderEndWidth: 3 });
410
+ expect(parseBorder("border-e-[5]")).toEqual({ borderEndWidth: 5 });
411
+ });
412
+ });
413
+
414
+ describe("parseBorder - logical border radius sides (RTL-aware)", () => {
415
+ it("should parse rounded start (both top and bottom start corners)", () => {
416
+ expect(parseBorder("rounded-s")).toEqual({
417
+ borderTopStartRadius: 4,
418
+ borderBottomStartRadius: 4,
419
+ });
420
+ expect(parseBorder("rounded-s-lg")).toEqual({
421
+ borderTopStartRadius: 8,
422
+ borderBottomStartRadius: 8,
423
+ });
424
+ expect(parseBorder("rounded-s-[12px]")).toEqual({
425
+ borderTopStartRadius: 12,
426
+ borderBottomStartRadius: 12,
427
+ });
428
+ });
429
+
430
+ it("should parse rounded end (both top and bottom end corners)", () => {
431
+ expect(parseBorder("rounded-e")).toEqual({
432
+ borderTopEndRadius: 4,
433
+ borderBottomEndRadius: 4,
434
+ });
435
+ expect(parseBorder("rounded-e-lg")).toEqual({
436
+ borderTopEndRadius: 8,
437
+ borderBottomEndRadius: 8,
438
+ });
439
+ expect(parseBorder("rounded-e-[12px]")).toEqual({
440
+ borderTopEndRadius: 12,
441
+ borderBottomEndRadius: 12,
442
+ });
443
+ });
444
+ });
445
+
446
+ describe("parseBorder - logical border radius corners (RTL-aware)", () => {
447
+ it("should parse rounded start-start (top-start corner)", () => {
448
+ expect(parseBorder("rounded-ss")).toEqual({ borderTopStartRadius: 4 });
449
+ expect(parseBorder("rounded-ss-lg")).toEqual({ borderTopStartRadius: 8 });
450
+ expect(parseBorder("rounded-ss-[12px]")).toEqual({
451
+ borderTopStartRadius: 12,
452
+ });
453
+ });
454
+
455
+ it("should parse rounded start-end (top-end corner)", () => {
456
+ expect(parseBorder("rounded-se")).toEqual({ borderTopEndRadius: 4 });
457
+ expect(parseBorder("rounded-se-lg")).toEqual({ borderTopEndRadius: 8 });
458
+ expect(parseBorder("rounded-se-[12px]")).toEqual({
459
+ borderTopEndRadius: 12,
460
+ });
461
+ });
462
+
463
+ it("should parse rounded end-start (bottom-start corner)", () => {
464
+ expect(parseBorder("rounded-es")).toEqual({ borderBottomStartRadius: 4 });
465
+ expect(parseBorder("rounded-es-lg")).toEqual({ borderBottomStartRadius: 8 });
466
+ expect(parseBorder("rounded-es-[12px]")).toEqual({
467
+ borderBottomStartRadius: 12,
468
+ });
469
+ });
470
+
471
+ it("should parse rounded end-end (bottom-end corner)", () => {
472
+ expect(parseBorder("rounded-ee")).toEqual({ borderBottomEndRadius: 4 });
473
+ expect(parseBorder("rounded-ee-lg")).toEqual({ borderBottomEndRadius: 8 });
474
+ expect(parseBorder("rounded-ee-[12px]")).toEqual({
475
+ borderBottomEndRadius: 12,
476
+ });
477
+ });
478
+
479
+ it("should parse all logical corners with different sizes", () => {
480
+ // Using full scale to verify all sizes work
481
+ expect(parseBorder("rounded-ss-none")).toEqual({ borderTopStartRadius: 0 });
482
+ expect(parseBorder("rounded-se-sm")).toEqual({ borderTopEndRadius: 2 });
483
+ expect(parseBorder("rounded-es-md")).toEqual({ borderBottomStartRadius: 6 });
484
+ expect(parseBorder("rounded-ee-xl")).toEqual({ borderBottomEndRadius: 12 });
485
+ expect(parseBorder("rounded-ss-2xl")).toEqual({ borderTopStartRadius: 16 });
486
+ expect(parseBorder("rounded-se-3xl")).toEqual({ borderTopEndRadius: 24 });
487
+ expect(parseBorder("rounded-es-full")).toEqual({
488
+ borderBottomStartRadius: 9999,
489
+ });
490
+ });
491
+ });
@@ -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> = {
@@ -34,10 +35,12 @@ const BORDER_WIDTH_PROP_MAP: Record<string, string> = {
34
35
  r: "borderRightWidth",
35
36
  b: "borderBottomWidth",
36
37
  l: "borderLeftWidth",
38
+ s: "borderStartWidth",
39
+ e: "borderEndWidth",
37
40
  };
38
41
 
39
42
  /**
40
- * Property mapping for border radius corners
43
+ * Property mapping for border radius corners (physical)
41
44
  */
42
45
  const BORDER_RADIUS_CORNER_MAP: Record<string, string> = {
43
46
  tl: "borderTopLeftRadius",
@@ -46,6 +49,18 @@ const BORDER_RADIUS_CORNER_MAP: Record<string, string> = {
46
49
  br: "borderBottomRightRadius",
47
50
  };
48
51
 
52
+ /**
53
+ * Property mapping for border radius corners (logical/RTL-aware)
54
+ * ss = start-start (top-start), se = start-end (top-end)
55
+ * es = end-start (bottom-start), ee = end-end (bottom-end)
56
+ */
57
+ const BORDER_RADIUS_LOGICAL_CORNER_MAP: Record<string, string> = {
58
+ ss: "borderTopStartRadius",
59
+ se: "borderTopEndRadius",
60
+ es: "borderBottomStartRadius",
61
+ ee: "borderBottomEndRadius",
62
+ };
63
+
49
64
  /**
50
65
  * Property mapping for border radius sides (returns array of properties)
51
66
  */
@@ -54,6 +69,8 @@ const BORDER_RADIUS_SIDE_MAP: Record<string, string[]> = {
54
69
  r: ["borderTopRightRadius", "borderBottomRightRadius"],
55
70
  b: ["borderBottomLeftRadius", "borderBottomRightRadius"],
56
71
  l: ["borderTopLeftRadius", "borderBottomLeftRadius"],
72
+ s: ["borderTopStartRadius", "borderBottomStartRadius"],
73
+ e: ["borderTopEndRadius", "borderBottomEndRadius"],
57
74
  };
58
75
 
59
76
  /**
@@ -108,8 +125,10 @@ function parseArbitraryBorderRadius(value: string): number | null {
108
125
 
109
126
  /**
110
127
  * Parse border classes
128
+ * @param cls - The class name to parse
129
+ * @param customColors - Optional custom colors from tailwind.config (used to detect color patterns)
111
130
  */
112
- export function parseBorder(cls: string): StyleObject | null {
131
+ export function parseBorder(cls: string, customColors?: Record<string, string>): StyleObject | null {
113
132
  // Border style (must come before parseBorderWidth)
114
133
  if (cls === "border-solid") return { borderStyle: "solid" };
115
134
  if (cls === "border-dotted") return { borderStyle: "dotted" };
@@ -117,7 +136,7 @@ export function parseBorder(cls: string): StyleObject | null {
117
136
 
118
137
  // Border width (border-0, border-t, border-[8px], etc.)
119
138
  if (cls.startsWith("border-")) {
120
- return parseBorderWidth(cls);
139
+ return parseBorderWidth(cls, customColors);
121
140
  }
122
141
 
123
142
  if (cls === "border") {
@@ -134,14 +153,27 @@ export function parseBorder(cls: string): StyleObject | null {
134
153
 
135
154
  /**
136
155
  * Parse border width classes
156
+ * @param cls - The class name to parse
157
+ * @param customColors - Optional custom colors (passed to parseColor for pattern detection)
137
158
  */
138
- function parseBorderWidth(cls: string): StyleObject | null {
139
- // Directional borders: border-t, border-t-2, border-t-[8px]
140
- const dirMatch = cls.match(/^border-([trbl])(?:-(.+))?$/);
159
+ function parseBorderWidth(cls: string, customColors?: Record<string, string>): StyleObject | null {
160
+ // Directional borders: border-t, border-t-2, border-t-[8px], border-s, border-e (RTL-aware)
161
+ // Note: border-x and border-y are handled by parseColor for colors only
162
+ const dirMatch = cls.match(/^border-([trblse])(?:-(.+))?$/);
141
163
  if (dirMatch) {
142
164
  const dir = dirMatch[1];
143
165
  const valueStr = dirMatch[2] || ""; // empty string for border-t
144
166
 
167
+ // If it's a color pattern, let parseColor handle it
168
+ // Try to parse as color - if it succeeds, return null (let parseColor handle it)
169
+ // Note: We skip color check for s/e since React Native doesn't support borderStartColor/borderEndColor
170
+ if (valueStr && dir !== "s" && dir !== "e") {
171
+ const colorResult = parseColor(cls, customColors);
172
+ if (colorResult !== null) {
173
+ return null; // It's a color, let parseColor handle it
174
+ }
175
+ }
176
+
145
177
  // Try arbitrary value first (if it starts with [)
146
178
  if (valueStr.startsWith("[")) {
147
179
  const arbitraryValue = parseArbitraryBorderWidth(valueStr);
@@ -205,7 +237,7 @@ function parseBorderRadius(cls: string): StyleObject | null {
205
237
  return null;
206
238
  }
207
239
 
208
- // Specific corners: rounded-tl, rounded-tl-lg, rounded-tl-[8px]
240
+ // Specific physical corners: rounded-tl, rounded-tl-lg, rounded-tl-[8px]
209
241
  const cornerMatch = rest.match(/^(tl|tr|bl|br)(?:-(.+))?$/);
210
242
  if (cornerMatch) {
211
243
  const corner = cornerMatch[1];
@@ -229,8 +261,34 @@ function parseBorderRadius(cls: string): StyleObject | null {
229
261
  return null;
230
262
  }
231
263
 
232
- // Sides: rounded-t, rounded-t-lg, rounded-t-[8px]
233
- const sideMatch = rest.match(/^([trbl])(?:-(.+))?$/);
264
+ // Logical corners (RTL-aware): rounded-ss, rounded-se, rounded-es, rounded-ee
265
+ // ss = start-start (top-start), se = start-end (top-end)
266
+ // es = end-start (bottom-start), ee = end-end (bottom-end)
267
+ const logicalCornerMatch = rest.match(/^(ss|se|es|ee)(?:-(.+))?$/);
268
+ if (logicalCornerMatch) {
269
+ const corner = logicalCornerMatch[1];
270
+ const valueStr = logicalCornerMatch[2] || ""; // empty string for rounded-ss
271
+
272
+ // Try arbitrary value first
273
+ if (valueStr.startsWith("[")) {
274
+ const arbitraryValue = parseArbitraryBorderRadius(valueStr);
275
+ if (arbitraryValue !== null) {
276
+ return { [BORDER_RADIUS_LOGICAL_CORNER_MAP[corner]]: arbitraryValue };
277
+ }
278
+ return null;
279
+ }
280
+
281
+ // Try preset scale
282
+ const scaleValue = BORDER_RADIUS_SCALE[valueStr];
283
+ if (scaleValue !== undefined) {
284
+ return { [BORDER_RADIUS_LOGICAL_CORNER_MAP[corner]]: scaleValue };
285
+ }
286
+
287
+ return null;
288
+ }
289
+
290
+ // Sides: rounded-t, rounded-t-lg, rounded-t-[8px], rounded-s, rounded-e (RTL-aware)
291
+ const sideMatch = rest.match(/^([trblse])(?:-(.+))?$/);
234
292
  if (sideMatch) {
235
293
  const side = sideMatch[1];
236
294
  const valueStr = sideMatch[2] || ""; // empty string for rounded-t