@mgcrea/react-native-tailwind 0.13.0 → 0.15.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 (73) hide show
  1. package/README.md +33 -30
  2. package/dist/babel/config-loader.d.ts +10 -0
  3. package/dist/babel/config-loader.test.ts +75 -21
  4. package/dist/babel/config-loader.ts +100 -2
  5. package/dist/babel/index.cjs +439 -46
  6. package/dist/babel/plugin/state.d.ts +4 -0
  7. package/dist/babel/plugin/state.ts +8 -0
  8. package/dist/babel/plugin/visitors/className.test.ts +313 -0
  9. package/dist/babel/plugin/visitors/className.ts +36 -8
  10. package/dist/babel/plugin/visitors/imports.ts +16 -1
  11. package/dist/babel/plugin/visitors/program.ts +19 -2
  12. package/dist/babel/plugin/visitors/tw.test.ts +151 -0
  13. package/dist/babel/utils/directionalModifierProcessing.d.ts +34 -0
  14. package/dist/babel/utils/directionalModifierProcessing.ts +99 -0
  15. package/dist/babel/utils/styleInjection.d.ts +16 -0
  16. package/dist/babel/utils/styleInjection.ts +138 -7
  17. package/dist/babel/utils/twProcessing.d.ts +2 -0
  18. package/dist/babel/utils/twProcessing.ts +92 -3
  19. package/dist/parser/borders.js +1 -1
  20. package/dist/parser/borders.test.js +1 -1
  21. package/dist/parser/index.d.ts +3 -2
  22. package/dist/parser/index.js +1 -1
  23. package/dist/parser/layout.d.ts +3 -1
  24. package/dist/parser/layout.js +1 -1
  25. package/dist/parser/layout.test.js +1 -1
  26. package/dist/parser/modifiers.d.ts +32 -2
  27. package/dist/parser/modifiers.js +1 -1
  28. package/dist/parser/modifiers.test.js +1 -1
  29. package/dist/parser/sizing.d.ts +3 -1
  30. package/dist/parser/sizing.js +1 -1
  31. package/dist/parser/sizing.test.js +1 -1
  32. package/dist/parser/spacing.d.ts +4 -2
  33. package/dist/parser/spacing.js +1 -1
  34. package/dist/parser/spacing.test.js +1 -1
  35. package/dist/parser/transforms.d.ts +3 -1
  36. package/dist/parser/transforms.js +1 -1
  37. package/dist/parser/transforms.test.js +1 -1
  38. package/dist/parser/typography.test.js +1 -1
  39. package/dist/runtime.cjs +1 -1
  40. package/dist/runtime.cjs.map +3 -3
  41. package/dist/runtime.d.ts +2 -0
  42. package/dist/runtime.js +1 -1
  43. package/dist/runtime.js.map +3 -3
  44. package/dist/runtime.test.js +1 -1
  45. package/package.json +6 -6
  46. package/src/babel/config-loader.test.ts +75 -21
  47. package/src/babel/config-loader.ts +100 -2
  48. package/src/babel/plugin/state.ts +8 -0
  49. package/src/babel/plugin/visitors/className.test.ts +313 -0
  50. package/src/babel/plugin/visitors/className.ts +36 -8
  51. package/src/babel/plugin/visitors/imports.ts +16 -1
  52. package/src/babel/plugin/visitors/program.ts +19 -2
  53. package/src/babel/plugin/visitors/tw.test.ts +151 -0
  54. package/src/babel/utils/directionalModifierProcessing.ts +99 -0
  55. package/src/babel/utils/styleInjection.ts +138 -7
  56. package/src/babel/utils/twProcessing.ts +92 -3
  57. package/src/parser/borders.test.ts +104 -0
  58. package/src/parser/borders.ts +50 -7
  59. package/src/parser/index.ts +8 -5
  60. package/src/parser/layout.test.ts +168 -0
  61. package/src/parser/layout.ts +107 -8
  62. package/src/parser/modifiers.test.ts +206 -0
  63. package/src/parser/modifiers.ts +62 -3
  64. package/src/parser/sizing.test.ts +56 -0
  65. package/src/parser/sizing.ts +20 -15
  66. package/src/parser/spacing.test.ts +123 -0
  67. package/src/parser/spacing.ts +30 -15
  68. package/src/parser/transforms.test.ts +57 -0
  69. package/src/parser/transforms.ts +7 -3
  70. package/src/parser/typography.test.ts +8 -0
  71. package/src/parser/typography.ts +4 -0
  72. package/src/runtime.test.ts +149 -0
  73. package/src/runtime.ts +53 -1
@@ -254,3 +254,59 @@ describe("parseSizing - comprehensive coverage", () => {
254
254
  expect(parseSizing("h-full")).toEqual({ height: "100%" });
255
255
  });
256
256
  });
257
+
258
+ describe("parseSizing - custom spacing", () => {
259
+ const customSpacing = {
260
+ xs: 4,
261
+ sm: 8,
262
+ md: 16,
263
+ lg: 32,
264
+ xl: 64,
265
+ "4": 20, // Override default (16)
266
+ };
267
+
268
+ it("should support custom spacing values for width", () => {
269
+ expect(parseSizing("w-xs", customSpacing)).toEqual({ width: 4 });
270
+ expect(parseSizing("w-sm", customSpacing)).toEqual({ width: 8 });
271
+ expect(parseSizing("w-lg", customSpacing)).toEqual({ width: 32 });
272
+ expect(parseSizing("w-xl", customSpacing)).toEqual({ width: 64 });
273
+ });
274
+
275
+ it("should support custom spacing values for height", () => {
276
+ expect(parseSizing("h-xs", customSpacing)).toEqual({ height: 4 });
277
+ expect(parseSizing("h-md", customSpacing)).toEqual({ height: 16 });
278
+ expect(parseSizing("h-xl", customSpacing)).toEqual({ height: 64 });
279
+ });
280
+
281
+ it("should support custom spacing values for min/max dimensions", () => {
282
+ expect(parseSizing("min-w-sm", customSpacing)).toEqual({ minWidth: 8 });
283
+ expect(parseSizing("min-h-lg", customSpacing)).toEqual({ minHeight: 32 });
284
+ expect(parseSizing("max-w-xl", customSpacing)).toEqual({ maxWidth: 64 });
285
+ expect(parseSizing("max-h-md", customSpacing)).toEqual({ maxHeight: 16 });
286
+ });
287
+
288
+ it("should allow custom spacing to override preset values", () => {
289
+ expect(parseSizing("w-4", customSpacing)).toEqual({ width: 20 }); // Custom 20, not default 16
290
+ expect(parseSizing("h-4", customSpacing)).toEqual({ height: 20 }); // Custom 20, not default 16
291
+ });
292
+
293
+ it("should prefer arbitrary values over custom spacing", () => {
294
+ expect(parseSizing("w-[24px]", customSpacing)).toEqual({ width: 24 }); // Arbitrary wins
295
+ expect(parseSizing("h-[50]", customSpacing)).toEqual({ height: 50 }); // Arbitrary wins
296
+ });
297
+
298
+ it("should fall back to preset scale for unknown custom keys", () => {
299
+ expect(parseSizing("w-8", customSpacing)).toEqual({ width: 32 }); // Not overridden, uses preset
300
+ expect(parseSizing("h-12", customSpacing)).toEqual({ height: 48 }); // Not overridden, uses preset
301
+ });
302
+
303
+ it("should preserve percentage values with custom spacing", () => {
304
+ expect(parseSizing("w-full", customSpacing)).toEqual({ width: "100%" });
305
+ expect(parseSizing("h-1/2", customSpacing)).toEqual({ height: "50%" });
306
+ });
307
+
308
+ it("should work without custom spacing (backward compatible)", () => {
309
+ expect(parseSizing("w-4")).toEqual({ width: 16 }); // Default behavior
310
+ expect(parseSizing("h-8")).toEqual({ height: 32 }); // Default behavior
311
+ });
312
+ });
@@ -95,8 +95,13 @@ function parseArbitrarySize(value: string): number | string | null {
95
95
 
96
96
  /**
97
97
  * Parse sizing classes
98
+ * @param cls - The class name to parse
99
+ * @param customSpacing - Optional custom spacing values from tailwind.config (shared with spacing utilities)
98
100
  */
99
- export function parseSizing(cls: string): StyleObject | null {
101
+ export function parseSizing(cls: string, customSpacing?: Record<string, number>): StyleObject | null {
102
+ // Merge custom spacing with defaults (custom takes precedence)
103
+ const sizeMap = customSpacing ? { ...SIZE_SCALE, ...customSpacing } : SIZE_SCALE;
104
+
100
105
  // Width
101
106
  if (cls.startsWith("w-")) {
102
107
  const sizeKey = cls.substring(2);
@@ -106,7 +111,7 @@ export function parseSizing(cls: string): StyleObject | null {
106
111
  return { width: `${RUNTIME_DIMENSIONS_MARKER}width}}` } as StyleObject;
107
112
  }
108
113
 
109
- // Arbitrary values: w-[123px], w-[50%]
114
+ // Arbitrary values: w-[123px], w-[50%] (highest priority)
110
115
  const arbitrarySize = parseArbitrarySize(sizeKey);
111
116
  if (arbitrarySize !== null) {
112
117
  return { width: arbitrarySize };
@@ -118,8 +123,8 @@ export function parseSizing(cls: string): StyleObject | null {
118
123
  return { width: percentage };
119
124
  }
120
125
 
121
- // Numeric widths: w-4, w-8, etc.
122
- const numericSize = SIZE_SCALE[sizeKey];
126
+ // Numeric widths: w-4, w-8, etc. (includes custom spacing)
127
+ const numericSize = sizeMap[sizeKey];
123
128
  if (numericSize !== undefined) {
124
129
  return { width: numericSize };
125
130
  }
@@ -139,7 +144,7 @@ export function parseSizing(cls: string): StyleObject | null {
139
144
  return { height: `${RUNTIME_DIMENSIONS_MARKER}height}}` } as StyleObject;
140
145
  }
141
146
 
142
- // Arbitrary values: h-[123px], h-[50%]
147
+ // Arbitrary values: h-[123px], h-[50%] (highest priority)
143
148
  const arbitrarySize = parseArbitrarySize(sizeKey);
144
149
  if (arbitrarySize !== null) {
145
150
  return { height: arbitrarySize };
@@ -151,8 +156,8 @@ export function parseSizing(cls: string): StyleObject | null {
151
156
  return { height: percentage };
152
157
  }
153
158
 
154
- // Numeric heights: h-4, h-8, etc.
155
- const numericSize = SIZE_SCALE[sizeKey];
159
+ // Numeric heights: h-4, h-8, etc. (includes custom spacing)
160
+ const numericSize = sizeMap[sizeKey];
156
161
  if (numericSize !== undefined) {
157
162
  return { height: numericSize };
158
163
  }
@@ -167,7 +172,7 @@ export function parseSizing(cls: string): StyleObject | null {
167
172
  if (cls.startsWith("min-w-")) {
168
173
  const sizeKey = cls.substring(6);
169
174
 
170
- // Arbitrary values: min-w-[123px], min-w-[50%]
175
+ // Arbitrary values: min-w-[123px], min-w-[50%] (highest priority)
171
176
  const arbitrarySize = parseArbitrarySize(sizeKey);
172
177
  if (arbitrarySize !== null) {
173
178
  return { minWidth: arbitrarySize };
@@ -178,7 +183,7 @@ export function parseSizing(cls: string): StyleObject | null {
178
183
  return { minWidth: percentage };
179
184
  }
180
185
 
181
- const numericSize = SIZE_SCALE[sizeKey];
186
+ const numericSize = sizeMap[sizeKey];
182
187
  if (numericSize !== undefined) {
183
188
  return { minWidth: numericSize };
184
189
  }
@@ -188,7 +193,7 @@ export function parseSizing(cls: string): StyleObject | null {
188
193
  if (cls.startsWith("min-h-")) {
189
194
  const sizeKey = cls.substring(6);
190
195
 
191
- // Arbitrary values: min-h-[123px], min-h-[50%]
196
+ // Arbitrary values: min-h-[123px], min-h-[50%] (highest priority)
192
197
  const arbitrarySize = parseArbitrarySize(sizeKey);
193
198
  if (arbitrarySize !== null) {
194
199
  return { minHeight: arbitrarySize };
@@ -199,7 +204,7 @@ export function parseSizing(cls: string): StyleObject | null {
199
204
  return { minHeight: percentage };
200
205
  }
201
206
 
202
- const numericSize = SIZE_SCALE[sizeKey];
207
+ const numericSize = sizeMap[sizeKey];
203
208
  if (numericSize !== undefined) {
204
209
  return { minHeight: numericSize };
205
210
  }
@@ -209,7 +214,7 @@ export function parseSizing(cls: string): StyleObject | null {
209
214
  if (cls.startsWith("max-w-")) {
210
215
  const sizeKey = cls.substring(6);
211
216
 
212
- // Arbitrary values: max-w-[123px], max-w-[50%]
217
+ // Arbitrary values: max-w-[123px], max-w-[50%] (highest priority)
213
218
  const arbitrarySize = parseArbitrarySize(sizeKey);
214
219
  if (arbitrarySize !== null) {
215
220
  return { maxWidth: arbitrarySize };
@@ -220,7 +225,7 @@ export function parseSizing(cls: string): StyleObject | null {
220
225
  return { maxWidth: percentage };
221
226
  }
222
227
 
223
- const numericSize = SIZE_SCALE[sizeKey];
228
+ const numericSize = sizeMap[sizeKey];
224
229
  if (numericSize !== undefined) {
225
230
  return { maxWidth: numericSize };
226
231
  }
@@ -230,7 +235,7 @@ export function parseSizing(cls: string): StyleObject | null {
230
235
  if (cls.startsWith("max-h-")) {
231
236
  const sizeKey = cls.substring(6);
232
237
 
233
- // Arbitrary values: max-h-[123px], max-h-[50%]
238
+ // Arbitrary values: max-h-[123px], max-h-[50%] (highest priority)
234
239
  const arbitrarySize = parseArbitrarySize(sizeKey);
235
240
  if (arbitrarySize !== null) {
236
241
  return { maxHeight: arbitrarySize };
@@ -241,7 +246,7 @@ export function parseSizing(cls: string): StyleObject | null {
241
246
  return { maxHeight: percentage };
242
247
  }
243
248
 
244
- const numericSize = SIZE_SCALE[sizeKey];
249
+ const numericSize = sizeMap[sizeKey];
245
250
  if (numericSize !== undefined) {
246
251
  return { maxHeight: numericSize };
247
252
  }
@@ -349,3 +349,126 @@ describe("parseSpacing - comprehensive coverage", () => {
349
349
  expect(parseSpacing("pl-[50px]")).toEqual({ paddingLeft: 50 });
350
350
  });
351
351
  });
352
+
353
+ describe("parseSpacing - logical margin (RTL-aware)", () => {
354
+ it("should parse margin start", () => {
355
+ expect(parseSpacing("ms-0")).toEqual({ marginStart: 0 });
356
+ expect(parseSpacing("ms-4")).toEqual({ marginStart: 16 });
357
+ expect(parseSpacing("ms-8")).toEqual({ marginStart: 32 });
358
+ });
359
+
360
+ it("should parse margin end", () => {
361
+ expect(parseSpacing("me-0")).toEqual({ marginEnd: 0 });
362
+ expect(parseSpacing("me-4")).toEqual({ marginEnd: 16 });
363
+ expect(parseSpacing("me-8")).toEqual({ marginEnd: 32 });
364
+ });
365
+
366
+ it("should parse margin start/end with fractional values", () => {
367
+ expect(parseSpacing("ms-0.5")).toEqual({ marginStart: 2 });
368
+ expect(parseSpacing("me-1.5")).toEqual({ marginEnd: 6 });
369
+ expect(parseSpacing("ms-2.5")).toEqual({ marginStart: 10 });
370
+ });
371
+
372
+ it("should parse margin start/end with arbitrary values", () => {
373
+ expect(parseSpacing("ms-[16px]")).toEqual({ marginStart: 16 });
374
+ expect(parseSpacing("ms-[16]")).toEqual({ marginStart: 16 });
375
+ expect(parseSpacing("me-[24px]")).toEqual({ marginEnd: 24 });
376
+ expect(parseSpacing("me-[24]")).toEqual({ marginEnd: 24 });
377
+ });
378
+
379
+ it("should parse negative margin start/end", () => {
380
+ expect(parseSpacing("-ms-4")).toEqual({ marginStart: -16 });
381
+ expect(parseSpacing("-me-4")).toEqual({ marginEnd: -16 });
382
+ expect(parseSpacing("-ms-8")).toEqual({ marginStart: -32 });
383
+ expect(parseSpacing("-me-8")).toEqual({ marginEnd: -32 });
384
+ });
385
+
386
+ it("should parse negative margin start/end with arbitrary values", () => {
387
+ expect(parseSpacing("-ms-[16px]")).toEqual({ marginStart: -16 });
388
+ expect(parseSpacing("-me-[24]")).toEqual({ marginEnd: -24 });
389
+ });
390
+ });
391
+
392
+ describe("parseSpacing - logical padding (RTL-aware)", () => {
393
+ it("should parse padding start", () => {
394
+ expect(parseSpacing("ps-0")).toEqual({ paddingStart: 0 });
395
+ expect(parseSpacing("ps-4")).toEqual({ paddingStart: 16 });
396
+ expect(parseSpacing("ps-8")).toEqual({ paddingStart: 32 });
397
+ });
398
+
399
+ it("should parse padding end", () => {
400
+ expect(parseSpacing("pe-0")).toEqual({ paddingEnd: 0 });
401
+ expect(parseSpacing("pe-4")).toEqual({ paddingEnd: 16 });
402
+ expect(parseSpacing("pe-8")).toEqual({ paddingEnd: 32 });
403
+ });
404
+
405
+ it("should parse padding start/end with fractional values", () => {
406
+ expect(parseSpacing("ps-0.5")).toEqual({ paddingStart: 2 });
407
+ expect(parseSpacing("pe-1.5")).toEqual({ paddingEnd: 6 });
408
+ expect(parseSpacing("ps-2.5")).toEqual({ paddingStart: 10 });
409
+ });
410
+
411
+ it("should parse padding start/end with arbitrary values", () => {
412
+ expect(parseSpacing("ps-[16px]")).toEqual({ paddingStart: 16 });
413
+ expect(parseSpacing("ps-[16]")).toEqual({ paddingStart: 16 });
414
+ expect(parseSpacing("pe-[24px]")).toEqual({ paddingEnd: 24 });
415
+ expect(parseSpacing("pe-[24]")).toEqual({ paddingEnd: 24 });
416
+ });
417
+ });
418
+
419
+ describe("parseSpacing - custom spacing", () => {
420
+ const customSpacing = {
421
+ xs: 4,
422
+ sm: 8,
423
+ md: 16,
424
+ lg: 32,
425
+ xl: 64,
426
+ "4": 20, // Override default (16)
427
+ };
428
+
429
+ it("should support custom spacing values for margin", () => {
430
+ expect(parseSpacing("m-xs", customSpacing)).toEqual({ margin: 4 });
431
+ expect(parseSpacing("m-sm", customSpacing)).toEqual({ margin: 8 });
432
+ expect(parseSpacing("m-lg", customSpacing)).toEqual({ margin: 32 });
433
+ expect(parseSpacing("mx-xl", customSpacing)).toEqual({ marginHorizontal: 64 });
434
+ expect(parseSpacing("mt-md", customSpacing)).toEqual({ marginTop: 16 });
435
+ });
436
+
437
+ it("should support custom spacing values for padding", () => {
438
+ expect(parseSpacing("p-xs", customSpacing)).toEqual({ padding: 4 });
439
+ expect(parseSpacing("p-sm", customSpacing)).toEqual({ padding: 8 });
440
+ expect(parseSpacing("px-lg", customSpacing)).toEqual({ paddingHorizontal: 32 });
441
+ expect(parseSpacing("pt-xl", customSpacing)).toEqual({ paddingTop: 64 });
442
+ });
443
+
444
+ it("should support custom spacing values for gap", () => {
445
+ expect(parseSpacing("gap-xs", customSpacing)).toEqual({ gap: 4 });
446
+ expect(parseSpacing("gap-md", customSpacing)).toEqual({ gap: 16 });
447
+ expect(parseSpacing("gap-xl", customSpacing)).toEqual({ gap: 64 });
448
+ });
449
+
450
+ it("should allow custom spacing to override preset values", () => {
451
+ expect(parseSpacing("m-4", customSpacing)).toEqual({ margin: 20 }); // Custom 20, not default 16
452
+ });
453
+
454
+ it("should prefer arbitrary values over custom spacing", () => {
455
+ expect(parseSpacing("m-[24px]", customSpacing)).toEqual({ margin: 24 }); // Arbitrary wins
456
+ expect(parseSpacing("p-[50]", customSpacing)).toEqual({ padding: 50 }); // Arbitrary wins
457
+ });
458
+
459
+ it("should support negative margins with custom spacing", () => {
460
+ expect(parseSpacing("-m-xs", customSpacing)).toEqual({ margin: -4 });
461
+ expect(parseSpacing("-m-lg", customSpacing)).toEqual({ margin: -32 });
462
+ expect(parseSpacing("-mx-xl", customSpacing)).toEqual({ marginHorizontal: -64 });
463
+ });
464
+
465
+ it("should fall back to preset scale for unknown custom keys", () => {
466
+ expect(parseSpacing("m-8", customSpacing)).toEqual({ margin: 32 }); // Not overridden, uses preset
467
+ expect(parseSpacing("p-12", customSpacing)).toEqual({ padding: 48 }); // Not overridden, uses preset
468
+ });
469
+
470
+ it("should work without custom spacing (backward compatible)", () => {
471
+ expect(parseSpacing("m-4")).toEqual({ margin: 16 }); // Default behavior
472
+ expect(parseSpacing("p-8")).toEqual({ padding: 32 }); // Default behavior
473
+ });
474
+ });
@@ -69,44 +69,51 @@ 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], pl-[4.5px], -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], ms-4, pe-2
73
+ * @param cls - The class name to parse
74
+ * @param customSpacing - Optional custom spacing values from tailwind.config
73
75
  */
74
- export function parseSpacing(cls: string): StyleObject | null {
75
- // Margin: m-4, mx-2, mt-8, m-[16px], -m-4, -mt-2, etc.
76
+ export function parseSpacing(cls: string, customSpacing?: Record<string, number>): StyleObject | null {
77
+ // Merge custom spacing with defaults (custom takes precedence)
78
+ const spacingMap = customSpacing ? { ...SPACING_SCALE, ...customSpacing } : SPACING_SCALE;
79
+
80
+ // Margin: m-4, mx-2, mt-8, ms-4, me-2, m-[16px], -m-4, -mt-2, etc.
76
81
  // Supports negative values for margins (but not padding or gap)
77
- const marginMatch = cls.match(/^(-?)m([xytrbls]?)-(.+)$/);
82
+ // s = start (RTL-aware), e = end (RTL-aware)
83
+ const marginMatch = cls.match(/^(-?)m([xytrblse]?)-(.+)$/);
78
84
  if (marginMatch) {
79
85
  const [, negativePrefix, dir, valueStr] = marginMatch;
80
86
  const isNegative = negativePrefix === "-";
81
87
 
82
- // Try arbitrary value first
88
+ // Try arbitrary value first (highest priority)
83
89
  const arbitraryValue = parseArbitrarySpacing(valueStr);
84
90
  if (arbitraryValue !== null) {
85
91
  const finalValue = isNegative ? -arbitraryValue : arbitraryValue;
86
92
  return getMarginStyle(dir, finalValue);
87
93
  }
88
94
 
89
- // Try preset scale
90
- const scaleValue = SPACING_SCALE[valueStr];
95
+ // Try spacing scale (includes custom spacing)
96
+ const scaleValue = spacingMap[valueStr];
91
97
  if (scaleValue !== undefined) {
92
98
  const finalValue = isNegative ? -scaleValue : scaleValue;
93
99
  return getMarginStyle(dir, finalValue);
94
100
  }
95
101
  }
96
102
 
97
- // Padding: p-4, px-2, pt-8, p-[16px], etc.
98
- const paddingMatch = cls.match(/^p([xytrbls]?)-(.+)$/);
103
+ // Padding: p-4, px-2, pt-8, ps-4, pe-2, p-[16px], etc.
104
+ // s = start (RTL-aware), e = end (RTL-aware)
105
+ const paddingMatch = cls.match(/^p([xytrblse]?)-(.+)$/);
99
106
  if (paddingMatch) {
100
107
  const [, dir, valueStr] = paddingMatch;
101
108
 
102
- // Try arbitrary value first
109
+ // Try arbitrary value first (highest priority)
103
110
  const arbitraryValue = parseArbitrarySpacing(valueStr);
104
111
  if (arbitraryValue !== null) {
105
112
  return getPaddingStyle(dir, arbitraryValue);
106
113
  }
107
114
 
108
- // Try preset scale
109
- const scaleValue = SPACING_SCALE[valueStr];
115
+ // Try spacing scale (includes custom spacing)
116
+ const scaleValue = spacingMap[valueStr];
110
117
  if (scaleValue !== undefined) {
111
118
  return getPaddingStyle(dir, scaleValue);
112
119
  }
@@ -117,14 +124,14 @@ export function parseSpacing(cls: string): StyleObject | null {
117
124
  if (gapMatch) {
118
125
  const valueStr = gapMatch[1];
119
126
 
120
- // Try arbitrary value first
127
+ // Try arbitrary value first (highest priority)
121
128
  const arbitraryValue = parseArbitrarySpacing(valueStr);
122
129
  if (arbitraryValue !== null) {
123
130
  return { gap: arbitraryValue };
124
131
  }
125
132
 
126
- // Try preset scale
127
- const scaleValue = SPACING_SCALE[valueStr];
133
+ // Try spacing scale (includes custom spacing)
134
+ const scaleValue = spacingMap[valueStr];
128
135
  if (scaleValue !== undefined) {
129
136
  return { gap: scaleValue };
130
137
  }
@@ -152,6 +159,10 @@ function getMarginStyle(dir: string, value: number): StyleObject {
152
159
  return { marginBottom: value };
153
160
  case "l":
154
161
  return { marginLeft: value };
162
+ case "s":
163
+ return { marginStart: value };
164
+ case "e":
165
+ return { marginEnd: value };
155
166
  default:
156
167
  return {};
157
168
  }
@@ -176,6 +187,10 @@ function getPaddingStyle(dir: string, value: number): StyleObject {
176
187
  return { paddingBottom: value };
177
188
  case "l":
178
189
  return { paddingLeft: value };
190
+ case "s":
191
+ return { paddingStart: value };
192
+ case "e":
193
+ return { paddingEnd: value };
179
194
  default:
180
195
  return {};
181
196
  }
@@ -316,3 +316,60 @@ describe("parseTransform - case sensitivity", () => {
316
316
  expect(parseTransform("ROTATE-45")).toBeNull();
317
317
  });
318
318
  });
319
+
320
+ describe("parseTransform - custom spacing", () => {
321
+ const customSpacing = {
322
+ xs: 4,
323
+ sm: 8,
324
+ md: 16,
325
+ lg: 32,
326
+ xl: 64,
327
+ "4": 20, // Override default (16)
328
+ };
329
+
330
+ it("should support custom spacing values for translateX", () => {
331
+ expect(parseTransform("translate-x-xs", customSpacing)).toEqual({ transform: [{ translateX: 4 }] });
332
+ expect(parseTransform("translate-x-sm", customSpacing)).toEqual({ transform: [{ translateX: 8 }] });
333
+ expect(parseTransform("translate-x-lg", customSpacing)).toEqual({ transform: [{ translateX: 32 }] });
334
+ expect(parseTransform("translate-x-xl", customSpacing)).toEqual({ transform: [{ translateX: 64 }] });
335
+ });
336
+
337
+ it("should support custom spacing values for translateY", () => {
338
+ expect(parseTransform("translate-y-xs", customSpacing)).toEqual({ transform: [{ translateY: 4 }] });
339
+ expect(parseTransform("translate-y-md", customSpacing)).toEqual({ transform: [{ translateY: 16 }] });
340
+ expect(parseTransform("translate-y-xl", customSpacing)).toEqual({ transform: [{ translateY: 64 }] });
341
+ });
342
+
343
+ it("should support negative custom spacing for translate", () => {
344
+ expect(parseTransform("-translate-x-sm", customSpacing)).toEqual({ transform: [{ translateX: -8 }] });
345
+ expect(parseTransform("-translate-y-lg", customSpacing)).toEqual({ transform: [{ translateY: -32 }] });
346
+ });
347
+
348
+ it("should allow custom spacing to override preset values", () => {
349
+ expect(parseTransform("translate-x-4", customSpacing)).toEqual({ transform: [{ translateX: 20 }] }); // Custom 20, not default 16
350
+ expect(parseTransform("translate-y-4", customSpacing)).toEqual({ transform: [{ translateY: 20 }] }); // Custom 20, not default 16
351
+ });
352
+
353
+ it("should prefer arbitrary values over custom spacing", () => {
354
+ expect(parseTransform("translate-x-[24px]", customSpacing)).toEqual({ transform: [{ translateX: 24 }] }); // Arbitrary wins
355
+ expect(parseTransform("translate-y-[50]", customSpacing)).toEqual({ transform: [{ translateY: 50 }] }); // Arbitrary wins
356
+ });
357
+
358
+ it("should fall back to preset scale for unknown custom keys", () => {
359
+ expect(parseTransform("translate-x-8", customSpacing)).toEqual({ transform: [{ translateX: 32 }] }); // Not overridden, uses preset
360
+ expect(parseTransform("translate-y-12", customSpacing)).toEqual({ transform: [{ translateY: 48 }] }); // Not overridden, uses preset
361
+ });
362
+
363
+ it("should work without custom spacing (backward compatible)", () => {
364
+ expect(parseTransform("translate-x-4")).toEqual({ transform: [{ translateX: 16 }] }); // Default behavior
365
+ expect(parseTransform("translate-y-8")).toEqual({ transform: [{ translateY: 32 }] }); // Default behavior
366
+ });
367
+
368
+ it("should not affect non-translate transforms", () => {
369
+ // Scale, rotate, skew, perspective should not use custom spacing
370
+ expect(parseTransform("scale-110", customSpacing)).toEqual({ transform: [{ scale: 1.1 }] });
371
+ expect(parseTransform("rotate-45", customSpacing)).toEqual({ transform: [{ rotate: "45deg" }] });
372
+ expect(parseTransform("skew-x-6", customSpacing)).toEqual({ transform: [{ skewX: "6deg" }] });
373
+ expect(parseTransform("perspective-500", customSpacing)).toEqual({ transform: [{ perspective: 500 }] });
374
+ });
375
+ });
@@ -164,8 +164,12 @@ function parseArbitraryPerspective(value: string): number | null {
164
164
  /**
165
165
  * Parse transform classes
166
166
  * Each transform class returns a transform array with a single transform object
167
+ * @param cls - The class name to parse
168
+ * @param customSpacing - Optional custom spacing values from tailwind.config (for translate utilities)
167
169
  */
168
- export function parseTransform(cls: string): StyleObject | null {
170
+ export function parseTransform(cls: string, customSpacing?: Record<string, number>): StyleObject | null {
171
+ // Merge custom spacing with defaults for translate utilities
172
+ const spacingMap = customSpacing ? { ...SPACING_SCALE, ...customSpacing } : SPACING_SCALE;
169
173
  // Transform origin warning (not supported in React Native)
170
174
  if (cls.startsWith("origin-")) {
171
175
  /* v8 ignore next 5 */
@@ -320,7 +324,7 @@ export function parseTransform(cls: string): StyleObject | null {
320
324
  return { transform: [{ translateX: value }] };
321
325
  }
322
326
 
323
- const translateValue = SPACING_SCALE[translateKey];
327
+ const translateValue = spacingMap[translateKey];
324
328
  if (translateValue !== undefined) {
325
329
  const value = isNegative ? -translateValue : translateValue;
326
330
  return { transform: [{ translateX: value }] };
@@ -346,7 +350,7 @@ export function parseTransform(cls: string): StyleObject | null {
346
350
  return { transform: [{ translateY: value }] };
347
351
  }
348
352
 
349
- const translateValue = SPACING_SCALE[translateKey];
353
+ const translateValue = spacingMap[translateKey];
350
354
  if (translateValue !== undefined) {
351
355
  const value = isNegative ? -translateValue : translateValue;
352
356
  return { transform: [{ translateY: value }] };
@@ -91,6 +91,14 @@ describe("parseTypography - text alignment", () => {
91
91
  expect(parseTypography("text-right")).toEqual({ textAlign: "right" });
92
92
  expect(parseTypography("text-justify")).toEqual({ textAlign: "justify" });
93
93
  });
94
+
95
+ it("should not parse text-start/text-end (handled via directional modifier expansion)", () => {
96
+ // text-start/text-end are now handled by splitModifierClasses() in modifiers.ts
97
+ // They expand to ltr:text-left rtl:text-right and ltr:text-right rtl:text-left
98
+ // respectively for true RTL support
99
+ expect(parseTypography("text-start")).toBeNull();
100
+ expect(parseTypography("text-end")).toBeNull();
101
+ });
94
102
  });
95
103
 
96
104
  describe("parseTypography - text decoration", () => {
@@ -58,6 +58,10 @@ const FONT_STYLE_MAP: Record<string, StyleObject> = {
58
58
  };
59
59
 
60
60
  // Text alignment utilities
61
+ // Note: text-start and text-end are handled via automatic expansion to directional modifiers
62
+ // in splitModifierClasses() for true RTL support:
63
+ // text-start -> ltr:text-left rtl:text-right
64
+ // text-end -> ltr:text-right rtl:text-left
61
65
  const TEXT_ALIGN_MAP: Record<string, StyleObject> = {
62
66
  "text-left": { textAlign: "left" },
63
67
  "text-center": { textAlign: "center" },