@mgcrea/react-native-tailwind 0.8.1 → 0.9.1

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 +152 -0
  2. package/dist/babel/config-loader.ts +2 -0
  3. package/dist/babel/index.cjs +205 -17
  4. package/dist/babel/plugin.d.ts +4 -1
  5. package/dist/babel/plugin.test.ts +327 -0
  6. package/dist/babel/plugin.ts +194 -14
  7. package/dist/babel/utils/platformModifierProcessing.d.ts +30 -0
  8. package/dist/babel/utils/platformModifierProcessing.ts +80 -0
  9. package/dist/babel/utils/styleInjection.d.ts +5 -1
  10. package/dist/babel/utils/styleInjection.ts +52 -7
  11. package/dist/babel/utils/styleTransforms.ts +1 -0
  12. package/dist/parser/aspectRatio.js +1 -1
  13. package/dist/parser/aspectRatio.test.js +1 -1
  14. package/dist/parser/index.d.ts +2 -2
  15. package/dist/parser/index.js +1 -1
  16. package/dist/parser/modifiers.d.ts +20 -2
  17. package/dist/parser/modifiers.js +1 -1
  18. package/dist/parser/spacing.d.ts +1 -1
  19. package/dist/parser/spacing.js +1 -1
  20. package/dist/parser/spacing.test.js +1 -1
  21. package/dist/runtime.cjs +1 -1
  22. package/dist/runtime.cjs.map +4 -4
  23. package/dist/runtime.js +1 -1
  24. package/dist/runtime.js.map +4 -4
  25. package/dist/runtime.test.js +1 -1
  26. package/dist/stubs/tw.test.js +1 -0
  27. package/package.json +7 -7
  28. package/src/babel/config-loader.ts +2 -0
  29. package/src/babel/plugin.test.ts +327 -0
  30. package/src/babel/plugin.ts +194 -14
  31. package/src/babel/utils/platformModifierProcessing.ts +80 -0
  32. package/src/babel/utils/styleInjection.ts +52 -7
  33. package/src/babel/utils/styleTransforms.ts +1 -0
  34. package/src/parser/aspectRatio.test.ts +25 -2
  35. package/src/parser/aspectRatio.ts +4 -3
  36. package/src/parser/borders.ts +2 -0
  37. package/src/parser/colors.ts +2 -0
  38. package/src/parser/index.ts +9 -2
  39. package/src/parser/layout.ts +2 -0
  40. package/src/parser/modifiers.ts +38 -4
  41. package/src/parser/placeholder.ts +1 -0
  42. package/src/parser/sizing.ts +1 -0
  43. package/src/parser/spacing.test.ts +63 -0
  44. package/src/parser/spacing.ts +11 -6
  45. package/src/parser/transforms.ts +5 -0
  46. package/src/parser/typography.ts +2 -0
  47. package/src/runtime.test.ts +27 -0
  48. package/src/runtime.ts +2 -1
  49. package/src/stubs/tw.test.ts +27 -0
  50. package/dist/babel/index.test.ts +0 -481
  51. package/dist/config/palettes.d.ts +0 -302
  52. package/dist/config/palettes.js +0 -1
  53. package/dist/parser/__snapshots__/aspectRatio.test.js.snap +0 -9
  54. package/dist/parser/__snapshots__/borders.test.js.snap +0 -23
  55. package/dist/parser/__snapshots__/colors.test.js.snap +0 -251
  56. package/dist/parser/__snapshots__/shadows.test.js.snap +0 -76
  57. package/dist/parser/__snapshots__/sizing.test.js.snap +0 -61
  58. package/dist/parser/__snapshots__/spacing.test.js.snap +0 -40
  59. package/dist/parser/__snapshots__/transforms.test.js.snap +0 -58
  60. package/dist/parser/__snapshots__/typography.test.js.snap +0 -30
  61. package/dist/parser/aspectRatio.test.d.ts +0 -1
  62. package/dist/parser/borders.test.d.ts +0 -1
  63. package/dist/parser/colors.test.d.ts +0 -1
  64. package/dist/parser/layout.test.d.ts +0 -1
  65. package/dist/parser/modifiers.test.d.ts +0 -1
  66. package/dist/parser/shadows.test.d.ts +0 -1
  67. package/dist/parser/sizing.test.d.ts +0 -1
  68. package/dist/parser/spacing.test.d.ts +0 -1
  69. package/dist/parser/typography.test.d.ts +0 -1
  70. package/dist/types.d.ts +0 -42
  71. package/dist/types.js +0 -1
@@ -80,6 +80,7 @@ function parseArbitrarySize(value: string): number | string | null {
80
80
 
81
81
  // Unsupported units (rem, em, vh, vw, etc.) - warn and reject
82
82
  if (value.startsWith("[") && value.endsWith("]")) {
83
+ /* v8 ignore next 5 */
83
84
  if (process.env.NODE_ENV !== "production") {
84
85
  console.warn(
85
86
  `[react-native-tailwind] Unsupported arbitrary size unit: ${value}. Only px and % are supported.`,
@@ -60,6 +60,69 @@ describe("parseSpacing - margin", () => {
60
60
  });
61
61
  });
62
62
 
63
+ describe("parseSpacing - negative margin", () => {
64
+ it("should parse negative margin all sides", () => {
65
+ expect(parseSpacing("-m-0")).toEqual({ margin: -0 }); // JavaScript -0 is distinct from +0
66
+ expect(parseSpacing("-m-4")).toEqual({ margin: -16 });
67
+ expect(parseSpacing("-m-8")).toEqual({ margin: -32 });
68
+ expect(parseSpacing("-m-96")).toEqual({ margin: -384 });
69
+ });
70
+
71
+ it("should parse negative margin with fractional values", () => {
72
+ expect(parseSpacing("-m-0.5")).toEqual({ margin: -2 });
73
+ expect(parseSpacing("-m-1.5")).toEqual({ margin: -6 });
74
+ expect(parseSpacing("-m-2.5")).toEqual({ margin: -10 });
75
+ });
76
+
77
+ it("should parse negative margin horizontal", () => {
78
+ expect(parseSpacing("-mx-4")).toEqual({ marginHorizontal: -16 });
79
+ expect(parseSpacing("-mx-8")).toEqual({ marginHorizontal: -32 });
80
+ });
81
+
82
+ it("should parse negative margin vertical", () => {
83
+ expect(parseSpacing("-my-4")).toEqual({ marginVertical: -16 });
84
+ expect(parseSpacing("-my-8")).toEqual({ marginVertical: -32 });
85
+ });
86
+
87
+ it("should parse negative margin directional", () => {
88
+ expect(parseSpacing("-mt-4")).toEqual({ marginTop: -16 });
89
+ expect(parseSpacing("-mr-4")).toEqual({ marginRight: -16 });
90
+ expect(parseSpacing("-mb-4")).toEqual({ marginBottom: -16 });
91
+ expect(parseSpacing("-ml-4")).toEqual({ marginLeft: -16 });
92
+ });
93
+
94
+ it("should parse negative margin with arbitrary values", () => {
95
+ expect(parseSpacing("-m-[16px]")).toEqual({ margin: -16 });
96
+ expect(parseSpacing("-m-[16]")).toEqual({ margin: -16 });
97
+ expect(parseSpacing("-m-[100px]")).toEqual({ margin: -100 });
98
+ expect(parseSpacing("-m-[100]")).toEqual({ margin: -100 });
99
+ });
100
+
101
+ it("should parse negative margin directional with arbitrary values", () => {
102
+ expect(parseSpacing("-mt-[24px]")).toEqual({ marginTop: -24 });
103
+ expect(parseSpacing("-mr-[32]")).toEqual({ marginRight: -32 });
104
+ expect(parseSpacing("-mb-[16px]")).toEqual({ marginBottom: -16 });
105
+ expect(parseSpacing("-ml-[48]")).toEqual({ marginLeft: -48 });
106
+ });
107
+
108
+ it("should parse negative margin horizontal/vertical with arbitrary values", () => {
109
+ expect(parseSpacing("-mx-[20px]")).toEqual({ marginHorizontal: -20 });
110
+ expect(parseSpacing("-my-[30]")).toEqual({ marginVertical: -30 });
111
+ });
112
+
113
+ it("should not parse negative padding (invalid)", () => {
114
+ expect(parseSpacing("-p-4")).toBeNull();
115
+ expect(parseSpacing("-px-4")).toBeNull();
116
+ expect(parseSpacing("-pt-4")).toBeNull();
117
+ expect(parseSpacing("-p-[16px]")).toBeNull();
118
+ });
119
+
120
+ it("should not parse negative gap (invalid)", () => {
121
+ expect(parseSpacing("-gap-4")).toBeNull();
122
+ expect(parseSpacing("-gap-[16px]")).toBeNull();
123
+ });
124
+ });
125
+
63
126
  describe("parseSpacing - padding", () => {
64
127
  it("should parse padding all sides", () => {
65
128
  expect(parseSpacing("p-0")).toEqual({ padding: 0 });
@@ -55,6 +55,7 @@ function parseArbitrarySpacing(value: string): number | null {
55
55
 
56
56
  // Warn about unsupported formats
57
57
  if (value.startsWith("[") && value.endsWith("]")) {
58
+ /* v8 ignore next 5 */
58
59
  if (process.env.NODE_ENV !== "production") {
59
60
  console.warn(
60
61
  `[react-native-tailwind] Unsupported arbitrary spacing value: ${value}. Only px values are supported (e.g., [16px] or [16]).`,
@@ -68,24 +69,28 @@ function parseArbitrarySpacing(value: string): number | null {
68
69
 
69
70
  /**
70
71
  * Parse spacing classes (margin, padding, gap)
71
- * Examples: m-4, mx-2, mt-8, p-4, px-2, pt-8, gap-4, m-[16px]
72
+ * Examples: m-4, mx-2, mt-8, p-4, px-2, pt-8, gap-4, m-[16px], -m-4, -mt-[10px]
72
73
  */
73
74
  export function parseSpacing(cls: string): StyleObject | null {
74
- // Margin: m-4, mx-2, mt-8, m-[16px], etc.
75
- const marginMatch = cls.match(/^m([xytrbls]?)-(.+)$/);
75
+ // Margin: m-4, mx-2, mt-8, m-[16px], -m-4, -mt-2, etc.
76
+ // Supports negative values for margins (but not padding or gap)
77
+ const marginMatch = cls.match(/^(-?)m([xytrbls]?)-(.+)$/);
76
78
  if (marginMatch) {
77
- const [, dir, valueStr] = marginMatch;
79
+ const [, negativePrefix, dir, valueStr] = marginMatch;
80
+ const isNegative = negativePrefix === "-";
78
81
 
79
82
  // Try arbitrary value first
80
83
  const arbitraryValue = parseArbitrarySpacing(valueStr);
81
84
  if (arbitraryValue !== null) {
82
- return getMarginStyle(dir, arbitraryValue);
85
+ const finalValue = isNegative ? -arbitraryValue : arbitraryValue;
86
+ return getMarginStyle(dir, finalValue);
83
87
  }
84
88
 
85
89
  // Try preset scale
86
90
  const scaleValue = SPACING_SCALE[valueStr];
87
91
  if (scaleValue !== undefined) {
88
- return getMarginStyle(dir, scaleValue);
92
+ const finalValue = isNegative ? -scaleValue : scaleValue;
93
+ return getMarginStyle(dir, finalValue);
89
94
  }
90
95
  }
91
96
 
@@ -70,6 +70,7 @@ function parseArbitraryScale(value: string): number | null {
70
70
 
71
71
  // Unsupported format
72
72
  if (value.startsWith("[") && value.endsWith("]")) {
73
+ /* v8 ignore next 5 */
73
74
  if (process.env.NODE_ENV !== "production") {
74
75
  console.warn(
75
76
  `[react-native-tailwind] Invalid arbitrary scale value: ${value}. Only numbers are supported (e.g., [1.5], [0.75]).`,
@@ -93,6 +94,7 @@ function parseArbitraryRotation(value: string): string | null {
93
94
 
94
95
  // Unsupported format
95
96
  if (value.startsWith("[") && value.endsWith("]")) {
97
+ /* v8 ignore next 5 */
96
98
  if (process.env.NODE_ENV !== "production") {
97
99
  console.warn(
98
100
  `[react-native-tailwind] Invalid arbitrary rotation value: ${value}. Only deg unit is supported (e.g., [45deg], [-15deg]).`,
@@ -123,6 +125,7 @@ function parseArbitraryTranslation(value: string): number | string | null {
123
125
 
124
126
  // Unsupported units
125
127
  if (value.startsWith("[") && value.endsWith("]")) {
128
+ /* v8 ignore next 5 */
126
129
  if (process.env.NODE_ENV !== "production") {
127
130
  console.warn(
128
131
  `[react-native-tailwind] Unsupported arbitrary translation unit: ${value}. Only px and % are supported.`,
@@ -146,6 +149,7 @@ function parseArbitraryPerspective(value: string): number | null {
146
149
 
147
150
  // Unsupported format
148
151
  if (value.startsWith("[") && value.endsWith("]")) {
152
+ /* v8 ignore next 5 */
149
153
  if (process.env.NODE_ENV !== "production") {
150
154
  console.warn(
151
155
  `[react-native-tailwind] Invalid arbitrary perspective value: ${value}. Only integers are supported (e.g., [1500]).`,
@@ -164,6 +168,7 @@ function parseArbitraryPerspective(value: string): number | null {
164
168
  export function parseTransform(cls: string): StyleObject | null {
165
169
  // Transform origin warning (not supported in React Native)
166
170
  if (cls.startsWith("origin-")) {
171
+ /* v8 ignore next 5 */
167
172
  if (process.env.NODE_ENV !== "production") {
168
173
  console.warn(
169
174
  `[react-native-tailwind] transform-origin is not supported in React Native. Class "${cls}" will be ignored.`,
@@ -125,6 +125,7 @@ function parseArbitraryFontSize(value: string): number | null {
125
125
 
126
126
  // Warn about unsupported formats
127
127
  if (value.startsWith("[") && value.endsWith("]")) {
128
+ /* v8 ignore next 5 */
128
129
  if (process.env.NODE_ENV !== "production") {
129
130
  console.warn(
130
131
  `[react-native-tailwind] Unsupported arbitrary font size value: ${value}. Only px values are supported (e.g., [18px] or [18]).`,
@@ -149,6 +150,7 @@ function parseArbitraryLineHeight(value: string): number | null {
149
150
 
150
151
  // Warn about unsupported formats
151
152
  if (value.startsWith("[") && value.endsWith("]")) {
153
+ /* v8 ignore next 5 */
152
154
  if (process.env.NODE_ENV !== "production") {
153
155
  console.warn(
154
156
  `[react-native-tailwind] Unsupported arbitrary line height value: ${value}. Only px values are supported (e.g., [24px] or [24]).`,
@@ -45,6 +45,33 @@ describe("runtime", () => {
45
45
  });
46
46
  });
47
47
 
48
+ it("should preserve zero values in template literals", () => {
49
+ // Issue #1 fix: 0 should be treated as valid value, not filtered out
50
+ const result = tw`opacity-${0} m-4`;
51
+ expect(result?.style).toEqual({
52
+ opacity: 0,
53
+ margin: 16,
54
+ });
55
+ });
56
+
57
+ it("should preserve empty string values in template literals", () => {
58
+ // Empty strings should be preserved (even though they don't contribute to className)
59
+ const result = tw`m-4 ${""}p-2`;
60
+ expect(result?.style).toEqual({
61
+ margin: 16,
62
+ padding: 8,
63
+ });
64
+ });
65
+
66
+ it("should handle mixed falsy and truthy numeric values", () => {
67
+ const spacing = 0;
68
+ const result = tw`m-${spacing} p-4`;
69
+ expect(result?.style).toEqual({
70
+ margin: 0,
71
+ padding: 16,
72
+ });
73
+ });
74
+
48
75
  it("should return empty style object for empty className", () => {
49
76
  const result = tw``;
50
77
  expect(result).toEqual({ style: {} });
package/src/runtime.ts CHANGED
@@ -211,8 +211,9 @@ export function tw<T extends NativeStyle = NativeStyle>(
211
211
  const className = strings.reduce((acc, str, i) => {
212
212
  const value = values[i];
213
213
  // Handle falsy values (false, null, undefined) - don't add them
214
+ // Note: 0 and empty string are preserved as they may be valid values
214
215
  // eslint-disable-next-line @typescript-eslint/no-base-to-string
215
- const valueStr = value ? String(value) : "";
216
+ const valueStr = value != null && value !== false ? String(value) : "";
216
217
  return acc + str + valueStr;
217
218
  }, "");
218
219
 
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { tw, twStyle } from "./tw";
3
+
4
+ describe("tw stub", () => {
5
+ it("should throw error when tw() is called without Babel transformation", () => {
6
+ expect(() => tw`bg-blue-500`).toThrow(
7
+ "tw() must be transformed by the Babel plugin. " +
8
+ "Ensure @mgcrea/react-native-tailwind/babel is configured in your babel.config.js. " +
9
+ "For runtime parsing, use: import { tw } from '@mgcrea/react-native-tailwind/runtime'",
10
+ );
11
+ });
12
+
13
+ it("should throw error when twStyle() is called without Babel transformation", () => {
14
+ expect(() => twStyle("bg-blue-500")).toThrow(
15
+ "twStyle() must be transformed by the Babel plugin. " +
16
+ "Ensure @mgcrea/react-native-tailwind/babel is configured in your babel.config.js. " +
17
+ "For runtime parsing, use: import { twStyle } from '@mgcrea/react-native-tailwind/runtime'",
18
+ );
19
+ });
20
+
21
+ it("should throw error with template literal interpolation", () => {
22
+ const dynamic = "active";
23
+ expect(() => tw`bg-blue-500 ${dynamic}:bg-blue-700`).toThrow(
24
+ "tw() must be transformed by the Babel plugin",
25
+ );
26
+ });
27
+ });