@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
@@ -4,7 +4,11 @@ import {
4
4
  expandSchemeModifier,
5
5
  hasModifier,
6
6
  isColorClass,
7
+ isColorSchemeModifier,
8
+ isDirectionalModifier,
9
+ isPlatformModifier,
7
10
  isSchemeModifier,
11
+ isStateModifier,
8
12
  parseModifier,
9
13
  splitModifierClasses,
10
14
  } from "./modifiers";
@@ -523,3 +527,205 @@ describe("expandSchemeModifier", () => {
523
527
  expect(result).toEqual([]);
524
528
  });
525
529
  });
530
+
531
+ describe("parseModifier - directional modifiers (rtl/ltr)", () => {
532
+ it("should parse rtl modifier", () => {
533
+ const result = parseModifier("rtl:text-right");
534
+ expect(result).toEqual({
535
+ modifier: "rtl",
536
+ baseClass: "text-right",
537
+ });
538
+ });
539
+
540
+ it("should parse ltr modifier", () => {
541
+ const result = parseModifier("ltr:text-left");
542
+ expect(result).toEqual({
543
+ modifier: "ltr",
544
+ baseClass: "text-left",
545
+ });
546
+ });
547
+
548
+ it("should parse directional modifiers with various base classes", () => {
549
+ expect(parseModifier("rtl:ms-4")).toEqual({
550
+ modifier: "rtl",
551
+ baseClass: "ms-4",
552
+ });
553
+ expect(parseModifier("ltr:me-4")).toEqual({
554
+ modifier: "ltr",
555
+ baseClass: "me-4",
556
+ });
557
+ expect(parseModifier("rtl:flex-row-reverse")).toEqual({
558
+ modifier: "rtl",
559
+ baseClass: "flex-row-reverse",
560
+ });
561
+ });
562
+ });
563
+
564
+ describe("modifier type check functions", () => {
565
+ it("should identify state modifiers", () => {
566
+ expect(isStateModifier("active")).toBe(true);
567
+ expect(isStateModifier("hover")).toBe(true);
568
+ expect(isStateModifier("focus")).toBe(true);
569
+ expect(isStateModifier("disabled")).toBe(true);
570
+ expect(isStateModifier("placeholder")).toBe(true);
571
+ expect(isStateModifier("ios")).toBe(false);
572
+ expect(isStateModifier("dark")).toBe(false);
573
+ expect(isStateModifier("rtl")).toBe(false);
574
+ });
575
+
576
+ it("should identify platform modifiers", () => {
577
+ expect(isPlatformModifier("ios")).toBe(true);
578
+ expect(isPlatformModifier("android")).toBe(true);
579
+ expect(isPlatformModifier("web")).toBe(true);
580
+ expect(isPlatformModifier("active")).toBe(false);
581
+ expect(isPlatformModifier("dark")).toBe(false);
582
+ expect(isPlatformModifier("rtl")).toBe(false);
583
+ });
584
+
585
+ it("should identify color scheme modifiers", () => {
586
+ expect(isColorSchemeModifier("dark")).toBe(true);
587
+ expect(isColorSchemeModifier("light")).toBe(true);
588
+ expect(isColorSchemeModifier("active")).toBe(false);
589
+ expect(isColorSchemeModifier("ios")).toBe(false);
590
+ expect(isColorSchemeModifier("rtl")).toBe(false);
591
+ });
592
+
593
+ it("should identify directional modifiers", () => {
594
+ expect(isDirectionalModifier("rtl")).toBe(true);
595
+ expect(isDirectionalModifier("ltr")).toBe(true);
596
+ expect(isDirectionalModifier("active")).toBe(false);
597
+ expect(isDirectionalModifier("ios")).toBe(false);
598
+ expect(isDirectionalModifier("dark")).toBe(false);
599
+ });
600
+
601
+ it("should identify scheme modifier", () => {
602
+ expect(isSchemeModifier("scheme")).toBe(true);
603
+ expect(isSchemeModifier("active")).toBe(false);
604
+ expect(isSchemeModifier("dark")).toBe(false);
605
+ expect(isSchemeModifier("rtl")).toBe(false);
606
+ });
607
+ });
608
+
609
+ describe("splitModifierClasses - directional modifiers", () => {
610
+ it("should split directional modifier classes", () => {
611
+ const result = splitModifierClasses("bg-white rtl:bg-gray-100 ltr:bg-gray-50");
612
+ expect(result.baseClasses).toEqual(["bg-white"]);
613
+ expect(result.modifierClasses).toHaveLength(2);
614
+ expect(result.modifierClasses[0]).toEqual({
615
+ modifier: "rtl",
616
+ baseClass: "bg-gray-100",
617
+ });
618
+ expect(result.modifierClasses[1]).toEqual({
619
+ modifier: "ltr",
620
+ baseClass: "bg-gray-50",
621
+ });
622
+ });
623
+
624
+ it("should handle mixed modifiers including directional", () => {
625
+ const result = splitModifierClasses("p-4 rtl:ps-6 ios:p-8 dark:bg-gray-900");
626
+ expect(result.baseClasses).toEqual(["p-4"]);
627
+ expect(result.modifierClasses).toHaveLength(3);
628
+ expect(result.modifierClasses.map((m) => m.modifier)).toContain("rtl");
629
+ expect(result.modifierClasses.map((m) => m.modifier)).toContain("ios");
630
+ expect(result.modifierClasses.map((m) => m.modifier)).toContain("dark");
631
+ });
632
+ });
633
+
634
+ describe("splitModifierClasses - text-start/text-end expansion", () => {
635
+ it("should expand text-start to ltr:text-left rtl:text-right", () => {
636
+ const result = splitModifierClasses("text-start");
637
+ expect(result.baseClasses).toEqual([]);
638
+ expect(result.modifierClasses).toHaveLength(2);
639
+ expect(result.modifierClasses).toContainEqual({
640
+ modifier: "ltr",
641
+ baseClass: "text-left",
642
+ });
643
+ expect(result.modifierClasses).toContainEqual({
644
+ modifier: "rtl",
645
+ baseClass: "text-right",
646
+ });
647
+ });
648
+
649
+ it("should expand text-end to ltr:text-right rtl:text-left", () => {
650
+ const result = splitModifierClasses("text-end");
651
+ expect(result.baseClasses).toEqual([]);
652
+ expect(result.modifierClasses).toHaveLength(2);
653
+ expect(result.modifierClasses).toContainEqual({
654
+ modifier: "ltr",
655
+ baseClass: "text-right",
656
+ });
657
+ expect(result.modifierClasses).toContainEqual({
658
+ modifier: "rtl",
659
+ baseClass: "text-left",
660
+ });
661
+ });
662
+
663
+ it("should handle text-start with other classes", () => {
664
+ const result = splitModifierClasses("p-4 text-start bg-white");
665
+ expect(result.baseClasses).toEqual(["p-4", "bg-white"]);
666
+ expect(result.modifierClasses).toHaveLength(2);
667
+ expect(result.modifierClasses).toContainEqual({
668
+ modifier: "ltr",
669
+ baseClass: "text-left",
670
+ });
671
+ expect(result.modifierClasses).toContainEqual({
672
+ modifier: "rtl",
673
+ baseClass: "text-right",
674
+ });
675
+ });
676
+
677
+ it("should handle both text-start and text-end in same className", () => {
678
+ // This is unusual but should work
679
+ const result = splitModifierClasses("text-start text-end");
680
+ expect(result.baseClasses).toEqual([]);
681
+ expect(result.modifierClasses).toHaveLength(4);
682
+ // text-start expands
683
+ expect(result.modifierClasses).toContainEqual({
684
+ modifier: "ltr",
685
+ baseClass: "text-left",
686
+ });
687
+ expect(result.modifierClasses).toContainEqual({
688
+ modifier: "rtl",
689
+ baseClass: "text-right",
690
+ });
691
+ // text-end expands
692
+ expect(result.modifierClasses).toContainEqual({
693
+ modifier: "ltr",
694
+ baseClass: "text-right",
695
+ });
696
+ expect(result.modifierClasses).toContainEqual({
697
+ modifier: "rtl",
698
+ baseClass: "text-left",
699
+ });
700
+ });
701
+
702
+ it("should not affect text-left and text-right (no expansion)", () => {
703
+ const result = splitModifierClasses("text-left text-right text-center");
704
+ expect(result.baseClasses).toEqual(["text-left", "text-right", "text-center"]);
705
+ expect(result.modifierClasses).toEqual([]);
706
+ });
707
+
708
+ it("should handle text-start/text-end with other modifiers", () => {
709
+ const result = splitModifierClasses("text-start active:bg-blue-500 rtl:pr-4");
710
+ expect(result.baseClasses).toEqual([]);
711
+ expect(result.modifierClasses).toHaveLength(4);
712
+ // text-start expansion
713
+ expect(result.modifierClasses).toContainEqual({
714
+ modifier: "ltr",
715
+ baseClass: "text-left",
716
+ });
717
+ expect(result.modifierClasses).toContainEqual({
718
+ modifier: "rtl",
719
+ baseClass: "text-right",
720
+ });
721
+ // explicit modifiers
722
+ expect(result.modifierClasses).toContainEqual({
723
+ modifier: "active",
724
+ baseClass: "bg-blue-500",
725
+ });
726
+ expect(result.modifierClasses).toContainEqual({
727
+ modifier: "rtl",
728
+ baseClass: "pr-4",
729
+ });
730
+ });
731
+ });
@@ -1,19 +1,22 @@
1
1
  /**
2
- * Modifier parsing utilities for state-based, platform-specific, and color scheme class names
2
+ * Modifier parsing utilities for state-based, platform-specific, color scheme, and directional class names
3
3
  * - State modifiers: active:, hover:, focus:, disabled:, placeholder:
4
4
  * - Platform modifiers: ios:, android:, web:
5
5
  * - Color scheme modifiers: dark:, light:
6
+ * - Directional modifiers: rtl:, ltr: (RTL-aware styling)
6
7
  */
7
8
 
8
9
  export type StateModifierType = "active" | "hover" | "focus" | "disabled" | "placeholder";
9
10
  export type PlatformModifierType = "ios" | "android" | "web";
10
11
  export type ColorSchemeModifierType = "dark" | "light";
11
12
  export type SchemeModifierType = "scheme";
13
+ export type DirectionalModifierType = "rtl" | "ltr";
12
14
  export type ModifierType =
13
15
  | StateModifierType
14
16
  | PlatformModifierType
15
17
  | ColorSchemeModifierType
16
- | SchemeModifierType;
18
+ | SchemeModifierType
19
+ | DirectionalModifierType;
17
20
 
18
21
  export type ParsedModifier = {
19
22
  modifier: ModifierType;
@@ -47,13 +50,19 @@ const COLOR_SCHEME_MODIFIERS: readonly ColorSchemeModifierType[] = ["dark", "lig
47
50
  const SCHEME_MODIFIERS: readonly SchemeModifierType[] = ["scheme"] as const;
48
51
 
49
52
  /**
50
- * All supported modifiers (state + platform + color scheme + scheme)
53
+ * Supported directional modifiers that map to I18nManager.isRTL values
54
+ */
55
+ const DIRECTIONAL_MODIFIERS: readonly DirectionalModifierType[] = ["rtl", "ltr"] as const;
56
+
57
+ /**
58
+ * All supported modifiers (state + platform + color scheme + scheme + directional)
51
59
  */
52
60
  const SUPPORTED_MODIFIERS: readonly ModifierType[] = [
53
61
  ...STATE_MODIFIERS,
54
62
  ...PLATFORM_MODIFIERS,
55
63
  ...COLOR_SCHEME_MODIFIERS,
56
64
  ...SCHEME_MODIFIERS,
65
+ ...DIRECTIONAL_MODIFIERS,
57
66
  ] as const;
58
67
 
59
68
  /**
@@ -149,6 +158,16 @@ export function isSchemeModifier(modifier: ModifierType): modifier is SchemeModi
149
158
  return SCHEME_MODIFIERS.includes(modifier as SchemeModifierType);
150
159
  }
151
160
 
161
+ /**
162
+ * Check if a modifier is a directional modifier (rtl, ltr)
163
+ *
164
+ * @param modifier - Modifier type to check
165
+ * @returns true if modifier is a directional modifier
166
+ */
167
+ export function isDirectionalModifier(modifier: ModifierType): modifier is DirectionalModifierType {
168
+ return DIRECTIONAL_MODIFIERS.includes(modifier as DirectionalModifierType);
169
+ }
170
+
152
171
  /**
153
172
  * Check if a class name is a color-based utility class
154
173
  *
@@ -242,6 +261,26 @@ export function expandSchemeModifier(
242
261
  ];
243
262
  }
244
263
 
264
+ /**
265
+ * Classes that expand to directional modifiers for true RTL support
266
+ * text-start -> ltr:text-left rtl:text-right (start = left in LTR, right in RTL)
267
+ * text-end -> ltr:text-right rtl:text-left (end = right in LTR, left in RTL)
268
+ */
269
+ const DIRECTIONAL_TEXT_ALIGN_EXPANSIONS: Record<string, { ltr: string; rtl: string }> = {
270
+ "text-start": { ltr: "text-left", rtl: "text-right" },
271
+ "text-end": { ltr: "text-right", rtl: "text-left" },
272
+ };
273
+
274
+ /**
275
+ * Check if a class should be expanded to directional modifiers
276
+ *
277
+ * @param cls - Class name to check
278
+ * @returns Expansion object if class should expand, undefined otherwise
279
+ */
280
+ export function getDirectionalExpansion(cls: string): { ltr: string; rtl: string } | undefined {
281
+ return DIRECTIONAL_TEXT_ALIGN_EXPANSIONS[cls];
282
+ }
283
+
245
284
  /**
246
285
  * Split a space-separated className string into base and modifier classes
247
286
  *
@@ -257,6 +296,17 @@ export function expandSchemeModifier(
257
296
  * // { modifier: "active", baseClass: "p-6" }
258
297
  * // ]
259
298
  * // }
299
+ *
300
+ * @example
301
+ * // text-start/text-end auto-expand to directional modifiers for true RTL support
302
+ * splitModifierClasses("text-start p-4")
303
+ * // {
304
+ * // baseClasses: ["p-4"],
305
+ * // modifierClasses: [
306
+ * // { modifier: "ltr", baseClass: "text-left" },
307
+ * // { modifier: "rtl", baseClass: "text-right" }
308
+ * // ]
309
+ * // }
260
310
  */
261
311
  export function splitModifierClasses(className: string): {
262
312
  baseClasses: string[];
@@ -267,6 +317,15 @@ export function splitModifierClasses(className: string): {
267
317
  const modifierClasses: ParsedModifier[] = [];
268
318
 
269
319
  for (const cls of classes) {
320
+ // Check for directional text alignment expansion (text-start, text-end)
321
+ const directionalExpansion = getDirectionalExpansion(cls);
322
+ if (directionalExpansion) {
323
+ // Expand to ltr: and rtl: modifiers for true RTL support
324
+ modifierClasses.push({ modifier: "ltr", baseClass: directionalExpansion.ltr });
325
+ modifierClasses.push({ modifier: "rtl", baseClass: directionalExpansion.rtl });
326
+ continue;
327
+ }
328
+
270
329
  const parsed = parseModifier(cls);
271
330
  if (parsed) {
272
331
  modifierClasses.push(parsed);
@@ -2,6 +2,7 @@
2
2
  * Sizing utilities (width, height, min/max)
3
3
  */
4
4
 
5
+ import { RUNTIME_DIMENSIONS_MARKER } from "../config/markers";
5
6
  import type { StyleObject } from "../types";
6
7
 
7
8
  // Size scale (in pixels/percentages)
@@ -100,6 +101,11 @@ export function parseSizing(cls: string): StyleObject | null {
100
101
  if (cls.startsWith("w-")) {
101
102
  const sizeKey = cls.substring(2);
102
103
 
104
+ // Screen width (requires runtime hook)
105
+ if (sizeKey === "screen") {
106
+ return { width: `${RUNTIME_DIMENSIONS_MARKER}width}}` } as StyleObject;
107
+ }
108
+
103
109
  // Arbitrary values: w-[123px], w-[50%]
104
110
  const arbitrarySize = parseArbitrarySize(sizeKey);
105
111
  if (arbitrarySize !== null) {
@@ -128,6 +134,11 @@ export function parseSizing(cls: string): StyleObject | null {
128
134
  if (cls.startsWith("h-")) {
129
135
  const sizeKey = cls.substring(2);
130
136
 
137
+ // Screen height (requires runtime hook)
138
+ if (sizeKey === "screen") {
139
+ return { height: `${RUNTIME_DIMENSIONS_MARKER}height}}` } as StyleObject;
140
+ }
141
+
131
142
  // Arbitrary values: h-[123px], h-[50%]
132
143
  const arbitrarySize = parseArbitrarySize(sizeKey);
133
144
  if (arbitrarySize !== null) {
@@ -349,3 +349,69 @@ 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
+ });
@@ -69,12 +69,13 @@ 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
73
  */
74
74
  export function parseSpacing(cls: string): StyleObject | null {
75
- // Margin: m-4, mx-2, mt-8, m-[16px], -m-4, -mt-2, etc.
75
+ // Margin: m-4, mx-2, mt-8, ms-4, me-2, m-[16px], -m-4, -mt-2, etc.
76
76
  // Supports negative values for margins (but not padding or gap)
77
- const marginMatch = cls.match(/^(-?)m([xytrbls]?)-(.+)$/);
77
+ // s = start (RTL-aware), e = end (RTL-aware)
78
+ const marginMatch = cls.match(/^(-?)m([xytrblse]?)-(.+)$/);
78
79
  if (marginMatch) {
79
80
  const [, negativePrefix, dir, valueStr] = marginMatch;
80
81
  const isNegative = negativePrefix === "-";
@@ -94,8 +95,9 @@ export function parseSpacing(cls: string): StyleObject | null {
94
95
  }
95
96
  }
96
97
 
97
- // Padding: p-4, px-2, pt-8, p-[16px], etc.
98
- const paddingMatch = cls.match(/^p([xytrbls]?)-(.+)$/);
98
+ // Padding: p-4, px-2, pt-8, ps-4, pe-2, p-[16px], etc.
99
+ // s = start (RTL-aware), e = end (RTL-aware)
100
+ const paddingMatch = cls.match(/^p([xytrblse]?)-(.+)$/);
99
101
  if (paddingMatch) {
100
102
  const [, dir, valueStr] = paddingMatch;
101
103
 
@@ -152,6 +154,10 @@ function getMarginStyle(dir: string, value: number): StyleObject {
152
154
  return { marginBottom: value };
153
155
  case "l":
154
156
  return { marginLeft: value };
157
+ case "s":
158
+ return { marginStart: value };
159
+ case "e":
160
+ return { marginEnd: value };
155
161
  default:
156
162
  return {};
157
163
  }
@@ -176,6 +182,10 @@ function getPaddingStyle(dir: string, value: number): StyleObject {
176
182
  return { paddingBottom: value };
177
183
  case "l":
178
184
  return { paddingLeft: value };
185
+ case "s":
186
+ return { paddingStart: value };
187
+ case "e":
188
+ return { paddingEnd: value };
179
189
  default:
180
190
  return {};
181
191
  }
@@ -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" },