@mgcrea/react-native-tailwind 0.11.1 → 0.12.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 (41) hide show
  1. package/dist/babel/config-loader.d.ts +3 -0
  2. package/dist/babel/config-loader.test.ts +2 -2
  3. package/dist/babel/config-loader.ts +37 -2
  4. package/dist/babel/index.cjs +325 -42
  5. package/dist/babel/plugin.test.ts +498 -0
  6. package/dist/babel/plugin.ts +66 -17
  7. package/dist/babel/utils/styleInjection.ts +57 -17
  8. package/dist/babel/utils/twProcessing.d.ts +8 -1
  9. package/dist/babel/utils/twProcessing.ts +212 -4
  10. package/dist/parser/index.d.ts +1 -0
  11. package/dist/parser/index.js +1 -1
  12. package/dist/parser/layout.js +1 -1
  13. package/dist/parser/layout.test.js +1 -1
  14. package/dist/parser/spacing.d.ts +1 -1
  15. package/dist/parser/spacing.js +1 -1
  16. package/dist/parser/spacing.test.js +1 -1
  17. package/dist/parser/typography.d.ts +2 -1
  18. package/dist/parser/typography.js +1 -1
  19. package/dist/parser/typography.test.js +1 -1
  20. package/dist/runtime.cjs +1 -1
  21. package/dist/runtime.cjs.map +3 -3
  22. package/dist/runtime.js +1 -1
  23. package/dist/runtime.js.map +3 -3
  24. package/dist/runtime.test.js +1 -1
  25. package/dist/types/runtime.d.ts +8 -1
  26. package/package.json +1 -1
  27. package/src/babel/config-loader.test.ts +2 -2
  28. package/src/babel/config-loader.ts +37 -2
  29. package/src/babel/plugin.test.ts +498 -0
  30. package/src/babel/plugin.ts +66 -17
  31. package/src/babel/utils/styleInjection.ts +57 -17
  32. package/src/babel/utils/twProcessing.ts +212 -4
  33. package/src/parser/index.ts +2 -1
  34. package/src/parser/layout.test.ts +61 -0
  35. package/src/parser/layout.ts +55 -1
  36. package/src/parser/spacing.test.ts +62 -0
  37. package/src/parser/spacing.ts +7 -7
  38. package/src/parser/typography.test.ts +102 -0
  39. package/src/parser/typography.ts +61 -15
  40. package/src/runtime.test.ts +4 -1
  41. package/src/types/runtime.ts +8 -1
@@ -60,6 +60,31 @@ function parseArbitraryZIndex(value: string): number | null {
60
60
  return null;
61
61
  }
62
62
 
63
+ /**
64
+ * Parse arbitrary grow/shrink value: [1.5], [2], [0.5], [.5]
65
+ * Returns number for valid non-negative values, null otherwise
66
+ */
67
+ function parseArbitraryGrowShrink(value: string): number | null {
68
+ // Match: [1.5], [2], [0], [0.5], [.5] (non-negative decimals, optional leading digit)
69
+ const match = value.match(/^\[(\d+(?:\.\d+)?|\.\d+)\]$/);
70
+ if (match) {
71
+ return parseFloat(match[1]);
72
+ }
73
+
74
+ // Warn about invalid formats (negative values, unsupported formats)
75
+ if (value.startsWith("[") && value.endsWith("]")) {
76
+ /* v8 ignore next 5 */
77
+ if (process.env.NODE_ENV !== "production") {
78
+ console.warn(
79
+ `[react-native-tailwind] Invalid arbitrary grow/shrink value: ${value}. Only non-negative numbers are supported (e.g., [1.5], [2], [0.5], [.5]).`,
80
+ );
81
+ }
82
+ return null;
83
+ }
84
+
85
+ return null;
86
+ }
87
+
63
88
  // Display utilities
64
89
  const DISPLAY_MAP: Record<string, StyleObject> = {
65
90
  flex: { display: "flex" },
@@ -88,12 +113,17 @@ const FLEX_MAP: Record<string, StyleObject> = {
88
113
  "flex-none": { flex: 0 },
89
114
  };
90
115
 
91
- // Flex grow/shrink utilities
116
+ // Flex grow/shrink utilities (includes CSS-style aliases)
92
117
  const GROW_SHRINK_MAP: Record<string, StyleObject> = {
93
118
  grow: { flexGrow: 1 },
94
119
  "grow-0": { flexGrow: 0 },
95
120
  shrink: { flexShrink: 1 },
96
121
  "shrink-0": { flexShrink: 0 },
122
+ // CSS-style aliases
123
+ "flex-grow": { flexGrow: 1 },
124
+ "flex-grow-0": { flexGrow: 0 },
125
+ "flex-shrink": { flexShrink: 1 },
126
+ "flex-shrink-0": { flexShrink: 0 },
97
127
  };
98
128
 
99
129
  // Justify content utilities
@@ -357,6 +387,30 @@ export function parseLayout(cls: string): StyleObject | null {
357
387
  }
358
388
  }
359
389
 
390
+ // Flex grow: grow-[1.5], flex-grow-[2], etc. (arbitrary values)
391
+ if (cls.startsWith("grow-") || cls.startsWith("flex-grow-")) {
392
+ const prefix = cls.startsWith("flex-grow-") ? "flex-grow-" : "grow-";
393
+ const growKey = cls.substring(prefix.length);
394
+
395
+ // Arbitrary values: grow-[1.5], flex-grow-[2]
396
+ const arbitraryGrow = parseArbitraryGrowShrink(growKey);
397
+ if (arbitraryGrow !== null) {
398
+ return { flexGrow: arbitraryGrow };
399
+ }
400
+ }
401
+
402
+ // Flex shrink: shrink-[0.5], flex-shrink-[1], etc. (arbitrary values)
403
+ if (cls.startsWith("shrink-") || cls.startsWith("flex-shrink-")) {
404
+ const prefix = cls.startsWith("flex-shrink-") ? "flex-shrink-" : "shrink-";
405
+ const shrinkKey = cls.substring(prefix.length);
406
+
407
+ // Arbitrary values: shrink-[0.5], flex-shrink-[1]
408
+ const arbitraryShrink = parseArbitraryGrowShrink(shrinkKey);
409
+ if (arbitraryShrink !== null) {
410
+ return { flexShrink: arbitraryShrink };
411
+ }
412
+ }
413
+
360
414
  // Try each lookup table in order
361
415
  return (
362
416
  DISPLAY_MAP[cls] ??
@@ -238,6 +238,68 @@ describe("parseSpacing - edge cases", () => {
238
238
  });
239
239
  });
240
240
 
241
+ describe("parseSpacing - decimal arbitrary values", () => {
242
+ it("should parse margin with decimal arbitrary values", () => {
243
+ expect(parseSpacing("m-[4.5px]")).toEqual({ margin: 4.5 });
244
+ expect(parseSpacing("m-[4.5]")).toEqual({ margin: 4.5 });
245
+ expect(parseSpacing("m-[16.75px]")).toEqual({ margin: 16.75 });
246
+ expect(parseSpacing("m-[16.75]")).toEqual({ margin: 16.75 });
247
+ expect(parseSpacing("m-[100.25px]")).toEqual({ margin: 100.25 });
248
+ expect(parseSpacing("m-[0.5]")).toEqual({ margin: 0.5 });
249
+ });
250
+
251
+ it("should parse padding with decimal arbitrary values", () => {
252
+ expect(parseSpacing("p-[4.5px]")).toEqual({ padding: 4.5 });
253
+ expect(parseSpacing("p-[4.5]")).toEqual({ padding: 4.5 });
254
+ expect(parseSpacing("pl-[4.5px]")).toEqual({ paddingLeft: 4.5 });
255
+ expect(parseSpacing("pl-[4.5]")).toEqual({ paddingLeft: 4.5 });
256
+ expect(parseSpacing("pr-[16.75px]")).toEqual({ paddingRight: 16.75 });
257
+ expect(parseSpacing("pt-[10.5]")).toEqual({ paddingTop: 10.5 });
258
+ expect(parseSpacing("pb-[20.25px]")).toEqual({ paddingBottom: 20.25 });
259
+ });
260
+
261
+ it("should parse padding horizontal/vertical with decimal arbitrary values", () => {
262
+ expect(parseSpacing("px-[4.5px]")).toEqual({ paddingHorizontal: 4.5 });
263
+ expect(parseSpacing("py-[10.75]")).toEqual({ paddingVertical: 10.75 });
264
+ });
265
+
266
+ it("should parse gap with decimal arbitrary values", () => {
267
+ expect(parseSpacing("gap-[4.5px]")).toEqual({ gap: 4.5 });
268
+ expect(parseSpacing("gap-[4.5]")).toEqual({ gap: 4.5 });
269
+ expect(parseSpacing("gap-[16.75px]")).toEqual({ gap: 16.75 });
270
+ expect(parseSpacing("gap-[0.5]")).toEqual({ gap: 0.5 });
271
+ });
272
+
273
+ it("should parse negative margin with decimal arbitrary values", () => {
274
+ expect(parseSpacing("-m-[4.5px]")).toEqual({ margin: -4.5 });
275
+ expect(parseSpacing("-m-[4.5]")).toEqual({ margin: -4.5 });
276
+ expect(parseSpacing("-m-[10.5px]")).toEqual({ margin: -10.5 });
277
+ expect(parseSpacing("-mt-[16.75px]")).toEqual({ marginTop: -16.75 });
278
+ expect(parseSpacing("-ml-[8.25]")).toEqual({ marginLeft: -8.25 });
279
+ expect(parseSpacing("-mx-[12.5px]")).toEqual({ marginHorizontal: -12.5 });
280
+ expect(parseSpacing("-my-[20.75]")).toEqual({ marginVertical: -20.75 });
281
+ });
282
+
283
+ it("should parse margin directional with decimal arbitrary values", () => {
284
+ expect(parseSpacing("mt-[4.5px]")).toEqual({ marginTop: 4.5 });
285
+ expect(parseSpacing("mr-[8.25]")).toEqual({ marginRight: 8.25 });
286
+ expect(parseSpacing("mb-[16.75px]")).toEqual({ marginBottom: 16.75 });
287
+ expect(parseSpacing("ml-[12.5]")).toEqual({ marginLeft: 12.5 });
288
+ });
289
+
290
+ it("should parse margin horizontal/vertical with decimal arbitrary values", () => {
291
+ expect(parseSpacing("mx-[4.5px]")).toEqual({ marginHorizontal: 4.5 });
292
+ expect(parseSpacing("my-[10.75]")).toEqual({ marginVertical: 10.75 });
293
+ });
294
+
295
+ it("should handle edge case decimal values", () => {
296
+ expect(parseSpacing("m-[0.1px]")).toEqual({ margin: 0.1 });
297
+ expect(parseSpacing("p-[0.001]")).toEqual({ padding: 0.001 });
298
+ expect(parseSpacing("gap-[999.999px]")).toEqual({ gap: 999.999 });
299
+ expect(parseSpacing("-m-[0.5]")).toEqual({ margin: -0.5 });
300
+ });
301
+ });
302
+
241
303
  describe("parseSpacing - comprehensive coverage", () => {
242
304
  it("should parse all margin directions with same value", () => {
243
305
  const value = 16;
@@ -43,14 +43,14 @@ export const SPACING_SCALE: Record<string, number> = {
43
43
  };
44
44
 
45
45
  /**
46
- * Parse arbitrary spacing value: [16px], [20]
47
- * Returns number for px values, null for unsupported formats
46
+ * Parse arbitrary spacing value: [16px], [20], [4.5px], [16.75]
47
+ * Returns number for px values (including decimals), null for unsupported formats
48
48
  */
49
49
  function parseArbitrarySpacing(value: string): number | null {
50
- // Match: [16px] or [16] (pixels only)
51
- const pxMatch = value.match(/^\[(\d+)(?:px)?\]$/);
50
+ // Match: [16px], [16], [4.5px], [4.5] (pixels, including decimals)
51
+ const pxMatch = value.match(/^\[(-?\d+(?:\.\d+)?)(?:px)?\]$/);
52
52
  if (pxMatch) {
53
- return parseInt(pxMatch[1], 10);
53
+ return parseFloat(pxMatch[1]);
54
54
  }
55
55
 
56
56
  // Warn about unsupported formats
@@ -58,7 +58,7 @@ function parseArbitrarySpacing(value: string): number | null {
58
58
  /* v8 ignore next 5 */
59
59
  if (process.env.NODE_ENV !== "production") {
60
60
  console.warn(
61
- `[react-native-tailwind] Unsupported arbitrary spacing value: ${value}. Only px values are supported (e.g., [16px] or [16]).`,
61
+ `[react-native-tailwind] Unsupported arbitrary spacing value: ${value}. Only px values are supported (e.g., [16px], [16], [4.5px], [4.5]).`,
62
62
  );
63
63
  }
64
64
  return null;
@@ -69,7 +69,7 @@ function parseArbitrarySpacing(value: string): number | null {
69
69
 
70
70
  /**
71
71
  * Parse spacing classes (margin, padding, gap)
72
- * Examples: m-4, mx-2, mt-8, p-4, px-2, pt-8, gap-4, m-[16px], -m-4, -mt-[10px]
72
+ * Examples: m-4, mx-2, mt-8, p-4, px-2, pt-8, gap-4, m-[16px], pl-[4.5px], -m-4, -mt-[10px]
73
73
  */
74
74
  export function parseSpacing(cls: string): StyleObject | null {
75
75
  // Margin: m-4, mx-2, mt-8, m-[16px], -m-4, -mt-2, etc.
@@ -39,6 +39,20 @@ describe("parseTypography - font size", () => {
39
39
  expect(parseTypography("text-[22]")).toEqual({ fontSize: 22 });
40
40
  expect(parseTypography("text-[100px]")).toEqual({ fontSize: 100 });
41
41
  });
42
+
43
+ it("should parse font size with decimal arbitrary values", () => {
44
+ expect(parseTypography("text-[13.5px]")).toEqual({ fontSize: 13.5 });
45
+ expect(parseTypography("text-[13.5]")).toEqual({ fontSize: 13.5 });
46
+ expect(parseTypography("text-[18.75px]")).toEqual({ fontSize: 18.75 });
47
+ expect(parseTypography("text-[18.75]")).toEqual({ fontSize: 18.75 });
48
+ expect(parseTypography("text-[22.5]")).toEqual({ fontSize: 22.5 });
49
+ });
50
+
51
+ it("should parse font size with Tailwind shorthand decimals", () => {
52
+ expect(parseTypography("text-[.5]")).toEqual({ fontSize: 0.5 });
53
+ expect(parseTypography("text-[.75px]")).toEqual({ fontSize: 0.75 });
54
+ expect(parseTypography("text-[.5px]")).toEqual({ fontSize: 0.5 });
55
+ });
42
56
  });
43
57
 
44
58
  describe("parseTypography - font family", () => {
@@ -136,6 +150,20 @@ describe("parseTypography - line height", () => {
136
150
  expect(parseTypography("leading-[30]")).toEqual({ lineHeight: 30 });
137
151
  expect(parseTypography("leading-[50px]")).toEqual({ lineHeight: 50 });
138
152
  });
153
+
154
+ it("should parse line height with decimal arbitrary values", () => {
155
+ expect(parseTypography("leading-[21.5px]")).toEqual({ lineHeight: 21.5 });
156
+ expect(parseTypography("leading-[21.5]")).toEqual({ lineHeight: 21.5 });
157
+ expect(parseTypography("leading-[28.75px]")).toEqual({ lineHeight: 28.75 });
158
+ expect(parseTypography("leading-[28.75]")).toEqual({ lineHeight: 28.75 });
159
+ expect(parseTypography("leading-[32.5]")).toEqual({ lineHeight: 32.5 });
160
+ });
161
+
162
+ it("should parse line height with Tailwind shorthand decimals", () => {
163
+ expect(parseTypography("leading-[.5]")).toEqual({ lineHeight: 0.5 });
164
+ expect(parseTypography("leading-[.75px]")).toEqual({ lineHeight: 0.75 });
165
+ expect(parseTypography("leading-[.5px]")).toEqual({ lineHeight: 0.5 });
166
+ });
139
167
  });
140
168
 
141
169
  describe("parseTypography - letter spacing", () => {
@@ -149,6 +177,80 @@ describe("parseTypography - letter spacing", () => {
149
177
  expect(parseTypography("tracking-wider")).toEqual({ letterSpacing: 0.8 });
150
178
  expect(parseTypography("tracking-widest")).toEqual({ letterSpacing: 1.6 });
151
179
  });
180
+
181
+ it("should parse letter spacing with arbitrary values", () => {
182
+ expect(parseTypography("tracking-[0.5px]")).toEqual({ letterSpacing: 0.5 });
183
+ expect(parseTypography("tracking-[0.5]")).toEqual({ letterSpacing: 0.5 });
184
+ expect(parseTypography("tracking-[0.3]")).toEqual({ letterSpacing: 0.3 });
185
+ expect(parseTypography("tracking-[1.2px]")).toEqual({ letterSpacing: 1.2 });
186
+ expect(parseTypography("tracking-[2]")).toEqual({ letterSpacing: 2 });
187
+ });
188
+
189
+ it("should parse letter spacing with Tailwind shorthand decimals", () => {
190
+ expect(parseTypography("tracking-[.5]")).toEqual({ letterSpacing: 0.5 });
191
+ expect(parseTypography("tracking-[.3px]")).toEqual({ letterSpacing: 0.3 });
192
+ expect(parseTypography("tracking-[.75]")).toEqual({ letterSpacing: 0.75 });
193
+ });
194
+
195
+ it("should parse negative letter spacing with arbitrary values", () => {
196
+ expect(parseTypography("tracking-[-0.4px]")).toEqual({ letterSpacing: -0.4 });
197
+ expect(parseTypography("tracking-[-0.4]")).toEqual({ letterSpacing: -0.4 });
198
+ expect(parseTypography("tracking-[-0.5]")).toEqual({ letterSpacing: -0.5 });
199
+ expect(parseTypography("tracking-[-1px]")).toEqual({ letterSpacing: -1 });
200
+ });
201
+
202
+ it("should parse negative letter spacing with shorthand decimals", () => {
203
+ expect(parseTypography("tracking-[-.4]")).toEqual({ letterSpacing: -0.4 });
204
+ expect(parseTypography("tracking-[-.5px]")).toEqual({ letterSpacing: -0.5 });
205
+ });
206
+
207
+ it("should handle edge case letter spacing values", () => {
208
+ expect(parseTypography("tracking-[0]")).toEqual({ letterSpacing: 0 });
209
+ expect(parseTypography("tracking-[0.1]")).toEqual({ letterSpacing: 0.1 });
210
+ expect(parseTypography("tracking-[10]")).toEqual({ letterSpacing: 10 });
211
+ });
212
+ });
213
+
214
+ describe("parseTypography - custom fontSize", () => {
215
+ const customFontSize = {
216
+ tiny: 10,
217
+ huge: 96,
218
+ xl: 22, // Override default (default is 20)
219
+ custom: 17,
220
+ };
221
+
222
+ it("should support custom font sizes", () => {
223
+ expect(parseTypography("text-tiny", undefined, customFontSize)).toEqual({ fontSize: 10 });
224
+ expect(parseTypography("text-huge", undefined, customFontSize)).toEqual({ fontSize: 96 });
225
+ expect(parseTypography("text-custom", undefined, customFontSize)).toEqual({ fontSize: 17 });
226
+ });
227
+
228
+ it("should allow custom fontSize to override preset sizes", () => {
229
+ expect(parseTypography("text-xl", undefined, customFontSize)).toEqual({ fontSize: 22 });
230
+ });
231
+
232
+ it("should fallback to preset sizes when custom fontSize not found", () => {
233
+ expect(parseTypography("text-base", undefined, customFontSize)).toEqual({ fontSize: 16 });
234
+ expect(parseTypography("text-lg", undefined, customFontSize)).toEqual({ fontSize: 18 });
235
+ expect(parseTypography("text-2xl", undefined, customFontSize)).toEqual({ fontSize: 24 });
236
+ });
237
+
238
+ it("should prefer arbitrary values over custom fontSize", () => {
239
+ expect(parseTypography("text-[15px]", undefined, customFontSize)).toEqual({ fontSize: 15 });
240
+ expect(parseTypography("text-[13.5]", undefined, customFontSize)).toEqual({ fontSize: 13.5 });
241
+ });
242
+
243
+ it("should work with both customFontFamily and customFontSize", () => {
244
+ const customFontFamily = { brand: "MyCustomFont" };
245
+ expect(parseTypography("text-tiny", customFontFamily, customFontSize)).toEqual({ fontSize: 10 });
246
+ expect(parseTypography("font-brand", customFontFamily, customFontSize)).toEqual({
247
+ fontFamily: "MyCustomFont",
248
+ });
249
+ });
250
+
251
+ it("should return null for unknown custom fontSize keys", () => {
252
+ expect(parseTypography("text-nonexistent", undefined, customFontSize)).toBeNull();
253
+ });
152
254
  });
153
255
 
154
256
  describe("parseTypography - edge cases", () => {
@@ -113,14 +113,14 @@ const TRACKING_MAP: Record<string, StyleObject> = {
113
113
  };
114
114
 
115
115
  /**
116
- * Parse arbitrary font size value: [18px], [20]
117
- * Returns number for px values, null for unsupported formats
116
+ * Parse arbitrary font size value: [18px], [20], [13.5px], [.5]
117
+ * Returns number for px values (including decimals), null for unsupported formats
118
118
  */
119
119
  function parseArbitraryFontSize(value: string): number | null {
120
- // Match: [18px] or [18] (pixels only)
121
- const pxMatch = value.match(/^\[(\d+)(?:px)?\]$/);
120
+ // Match: [18px], [18], [13.5px], [13.5], [.5] (pixels, including decimals)
121
+ const pxMatch = value.match(/^\[(-?\d+(?:\.\d+)?|-?\.\d+)(?:px)?\]$/);
122
122
  if (pxMatch) {
123
- return parseInt(pxMatch[1], 10);
123
+ return parseFloat(pxMatch[1]);
124
124
  }
125
125
 
126
126
  // Warn about unsupported formats
@@ -128,7 +128,7 @@ function parseArbitraryFontSize(value: string): number | null {
128
128
  /* v8 ignore next 5 */
129
129
  if (process.env.NODE_ENV !== "production") {
130
130
  console.warn(
131
- `[react-native-tailwind] Unsupported arbitrary font size value: ${value}. Only px values are supported (e.g., [18px] or [18]).`,
131
+ `[react-native-tailwind] Unsupported arbitrary font size value: ${value}. Only px values are supported (e.g., [18px], [13.5px], [.5]).`,
132
132
  );
133
133
  }
134
134
  return null;
@@ -138,14 +138,14 @@ function parseArbitraryFontSize(value: string): number | null {
138
138
  }
139
139
 
140
140
  /**
141
- * Parse arbitrary line height value: [24px], [28]
142
- * Returns number for px values, null for unsupported formats
141
+ * Parse arbitrary line height value: [24px], [28], [21.5px], [.5]
142
+ * Returns number for px values (including decimals), null for unsupported formats
143
143
  */
144
144
  function parseArbitraryLineHeight(value: string): number | null {
145
- // Match: [24px] or [24] (pixels only)
146
- const pxMatch = value.match(/^\[(\d+)(?:px)?\]$/);
145
+ // Match: [24px], [24], [21.5px], [21.5], [.5] (pixels, including decimals)
146
+ const pxMatch = value.match(/^\[(-?\d+(?:\.\d+)?|-?\.\d+)(?:px)?\]$/);
147
147
  if (pxMatch) {
148
- return parseInt(pxMatch[1], 10);
148
+ return parseFloat(pxMatch[1]);
149
149
  }
150
150
 
151
151
  // Warn about unsupported formats
@@ -153,7 +153,32 @@ function parseArbitraryLineHeight(value: string): number | null {
153
153
  /* v8 ignore next 5 */
154
154
  if (process.env.NODE_ENV !== "production") {
155
155
  console.warn(
156
- `[react-native-tailwind] Unsupported arbitrary line height value: ${value}. Only px values are supported (e.g., [24px] or [24]).`,
156
+ `[react-native-tailwind] Unsupported arbitrary line height value: ${value}. Only px values are supported (e.g., [24px], [21.5px], [.5]).`,
157
+ );
158
+ }
159
+ return null;
160
+ }
161
+
162
+ return null;
163
+ }
164
+
165
+ /**
166
+ * Parse arbitrary letter spacing value: [0.5px], [0.3], [.5], [-0.4]
167
+ * Returns number for px values (including decimals), null for unsupported formats
168
+ */
169
+ function parseArbitraryLetterSpacing(value: string): number | null {
170
+ // Match: [0.5px], [0.3], [.5], [-0.4px] (pixels, including decimals and negatives)
171
+ const pxMatch = value.match(/^\[(-?\d+(?:\.\d+)?|-?\.\d+)(?:px)?\]$/);
172
+ if (pxMatch) {
173
+ return parseFloat(pxMatch[1]);
174
+ }
175
+
176
+ // Warn about unsupported formats
177
+ if (value.startsWith("[") && value.endsWith("]")) {
178
+ /* v8 ignore next 5 */
179
+ if (process.env.NODE_ENV !== "production") {
180
+ console.warn(
181
+ `[react-native-tailwind] Unsupported arbitrary letter spacing value: ${value}. Only px values are supported (e.g., [0.5px], [0.3], [.5], [-0.4]).`,
157
182
  );
158
183
  }
159
184
  return null;
@@ -166,8 +191,13 @@ function parseArbitraryLineHeight(value: string): number | null {
166
191
  * Parse typography classes
167
192
  * @param cls - Class name to parse
168
193
  * @param customFontFamily - Optional custom fontFamily from tailwind.config
194
+ * @param customFontSize - Optional custom fontSize from tailwind.config
169
195
  */
170
- export function parseTypography(cls: string, customFontFamily?: Record<string, string>): StyleObject | null {
196
+ export function parseTypography(
197
+ cls: string,
198
+ customFontFamily?: Record<string, string>,
199
+ customFontSize?: Record<string, number>,
200
+ ): StyleObject | null {
171
201
  // Merge custom fontFamily with defaults (custom takes precedence)
172
202
  const fontFamilyMap = customFontFamily
173
203
  ? {
@@ -182,13 +212,18 @@ export function parseTypography(cls: string, customFontFamily?: Record<string, s
182
212
  if (cls.startsWith("text-")) {
183
213
  const sizeKey = cls.substring(5);
184
214
 
185
- // Try arbitrary value first
215
+ // Try arbitrary value first (highest priority)
186
216
  const arbitraryValue = parseArbitraryFontSize(sizeKey);
187
217
  if (arbitraryValue !== null) {
188
218
  return { fontSize: arbitraryValue };
189
219
  }
190
220
 
191
- // Try preset scale
221
+ // Try custom fontSize from config
222
+ if (customFontSize?.[sizeKey] !== undefined) {
223
+ return { fontSize: customFontSize[sizeKey] };
224
+ }
225
+
226
+ // Try preset scale (fallback)
192
227
  const fontSize = FONT_SIZES[sizeKey];
193
228
  if (fontSize !== undefined) {
194
229
  return { fontSize };
@@ -212,6 +247,17 @@ export function parseTypography(cls: string, customFontFamily?: Record<string, s
212
247
  }
213
248
  }
214
249
 
250
+ // Letter spacing: tracking-wide, tracking-[0.5px], tracking-[.3], etc.
251
+ if (cls.startsWith("tracking-")) {
252
+ const trackingKey = cls.substring(9);
253
+
254
+ // Try arbitrary value first
255
+ const arbitraryValue = parseArbitraryLetterSpacing(trackingKey);
256
+ if (arbitraryValue !== null) {
257
+ return { letterSpacing: arbitraryValue };
258
+ }
259
+ }
260
+
215
261
  // Try each lookup table in order
216
262
  return (
217
263
  fontFamilyMap[cls] ??
@@ -317,7 +317,10 @@ describe("runtime", () => {
317
317
  it("should provide raw hex values for animations", () => {
318
318
  const result = tw`bg-blue-500 active:bg-blue-700`;
319
319
  // Access raw backgroundColor value for use with reanimated
320
- expect(result?.style.backgroundColor).toBe("#2b7fff");
320
+ const style = Array.isArray(result?.style) ? result.style.find((s) => s !== false) : result?.style;
321
+ expect(
322
+ style && typeof style === "object" && "backgroundColor" in style ? style.backgroundColor : undefined,
323
+ ).toBe("#2b7fff");
321
324
  expect(result?.activeStyle?.backgroundColor).toBe("#1447e6");
322
325
  });
323
326
 
@@ -7,11 +7,18 @@ export type NativeStyle = ViewStyle | TextStyle | ImageStyle;
7
7
 
8
8
  /**
9
9
  * Return type for tw/twStyle functions with separate style properties for modifiers
10
+ * When color-scheme modifiers (dark:, light:) are present, style becomes an array with runtime conditionals
11
+ * When platform modifiers (ios:, android:, web:) are present, style becomes an array with Platform.select()
10
12
  */
11
13
  export type TwStyle<T extends NativeStyle = NativeStyle> = {
12
- style: T;
14
+ style: T | Array<T | false>;
13
15
  activeStyle?: T;
14
16
  focusStyle?: T;
15
17
  disabledStyle?: T;
16
18
  placeholderStyle?: TextStyle;
19
+ lightStyle?: T;
20
+ darkStyle?: T;
21
+ iosStyle?: T;
22
+ androidStyle?: T;
23
+ webStyle?: T;
17
24
  };