@mgcrea/react-native-tailwind 0.13.0 → 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 (53) hide show
  1. package/README.md +21 -22
  2. package/dist/babel/index.cjs +342 -17
  3. package/dist/babel/plugin/state.d.ts +4 -0
  4. package/dist/babel/plugin/state.ts +8 -0
  5. package/dist/babel/plugin/visitors/className.test.ts +313 -0
  6. package/dist/babel/plugin/visitors/className.ts +36 -8
  7. package/dist/babel/plugin/visitors/imports.ts +16 -1
  8. package/dist/babel/plugin/visitors/program.ts +19 -2
  9. package/dist/babel/plugin/visitors/tw.test.ts +151 -0
  10. package/dist/babel/utils/directionalModifierProcessing.d.ts +34 -0
  11. package/dist/babel/utils/directionalModifierProcessing.ts +99 -0
  12. package/dist/babel/utils/styleInjection.d.ts +16 -0
  13. package/dist/babel/utils/styleInjection.ts +138 -7
  14. package/dist/babel/utils/twProcessing.d.ts +2 -0
  15. package/dist/babel/utils/twProcessing.ts +92 -3
  16. package/dist/parser/borders.js +1 -1
  17. package/dist/parser/borders.test.js +1 -1
  18. package/dist/parser/index.d.ts +2 -2
  19. package/dist/parser/index.js +1 -1
  20. package/dist/parser/layout.js +1 -1
  21. package/dist/parser/layout.test.js +1 -1
  22. package/dist/parser/modifiers.d.ts +32 -2
  23. package/dist/parser/modifiers.js +1 -1
  24. package/dist/parser/modifiers.test.js +1 -1
  25. package/dist/parser/spacing.d.ts +1 -1
  26. package/dist/parser/spacing.js +1 -1
  27. package/dist/parser/spacing.test.js +1 -1
  28. package/dist/parser/typography.test.js +1 -1
  29. package/dist/runtime.cjs +1 -1
  30. package/dist/runtime.cjs.map +3 -3
  31. package/dist/runtime.js +1 -1
  32. package/dist/runtime.js.map +3 -3
  33. package/package.json +6 -6
  34. package/src/babel/plugin/state.ts +8 -0
  35. package/src/babel/plugin/visitors/className.test.ts +313 -0
  36. package/src/babel/plugin/visitors/className.ts +36 -8
  37. package/src/babel/plugin/visitors/imports.ts +16 -1
  38. package/src/babel/plugin/visitors/program.ts +19 -2
  39. package/src/babel/plugin/visitors/tw.test.ts +151 -0
  40. package/src/babel/utils/directionalModifierProcessing.ts +99 -0
  41. package/src/babel/utils/styleInjection.ts +138 -7
  42. package/src/babel/utils/twProcessing.ts +92 -3
  43. package/src/parser/borders.test.ts +104 -0
  44. package/src/parser/borders.ts +50 -7
  45. package/src/parser/index.ts +2 -0
  46. package/src/parser/layout.test.ts +74 -0
  47. package/src/parser/layout.ts +94 -0
  48. package/src/parser/modifiers.test.ts +206 -0
  49. package/src/parser/modifiers.ts +62 -3
  50. package/src/parser/spacing.test.ts +66 -0
  51. package/src/parser/spacing.ts +15 -5
  52. package/src/parser/typography.test.ts +8 -0
  53. package/src/parser/typography.ts +4 -0
@@ -35,10 +35,12 @@ const BORDER_WIDTH_PROP_MAP: Record<string, string> = {
35
35
  r: "borderRightWidth",
36
36
  b: "borderBottomWidth",
37
37
  l: "borderLeftWidth",
38
+ s: "borderStartWidth",
39
+ e: "borderEndWidth",
38
40
  };
39
41
 
40
42
  /**
41
- * Property mapping for border radius corners
43
+ * Property mapping for border radius corners (physical)
42
44
  */
43
45
  const BORDER_RADIUS_CORNER_MAP: Record<string, string> = {
44
46
  tl: "borderTopLeftRadius",
@@ -47,6 +49,18 @@ const BORDER_RADIUS_CORNER_MAP: Record<string, string> = {
47
49
  br: "borderBottomRightRadius",
48
50
  };
49
51
 
52
+ /**
53
+ * Property mapping for border radius corners (logical/RTL-aware)
54
+ * ss = start-start (top-start), se = start-end (top-end)
55
+ * es = end-start (bottom-start), ee = end-end (bottom-end)
56
+ */
57
+ const BORDER_RADIUS_LOGICAL_CORNER_MAP: Record<string, string> = {
58
+ ss: "borderTopStartRadius",
59
+ se: "borderTopEndRadius",
60
+ es: "borderBottomStartRadius",
61
+ ee: "borderBottomEndRadius",
62
+ };
63
+
50
64
  /**
51
65
  * Property mapping for border radius sides (returns array of properties)
52
66
  */
@@ -55,6 +69,8 @@ const BORDER_RADIUS_SIDE_MAP: Record<string, string[]> = {
55
69
  r: ["borderTopRightRadius", "borderBottomRightRadius"],
56
70
  b: ["borderBottomLeftRadius", "borderBottomRightRadius"],
57
71
  l: ["borderTopLeftRadius", "borderBottomLeftRadius"],
72
+ s: ["borderTopStartRadius", "borderBottomStartRadius"],
73
+ e: ["borderTopEndRadius", "borderBottomEndRadius"],
58
74
  };
59
75
 
60
76
  /**
@@ -141,16 +157,17 @@ export function parseBorder(cls: string, customColors?: Record<string, string>):
141
157
  * @param customColors - Optional custom colors (passed to parseColor for pattern detection)
142
158
  */
143
159
  function parseBorderWidth(cls: string, customColors?: Record<string, string>): StyleObject | null {
144
- // Directional borders: border-t, border-t-2, border-t-[8px]
160
+ // Directional borders: border-t, border-t-2, border-t-[8px], border-s, border-e (RTL-aware)
145
161
  // Note: border-x and border-y are handled by parseColor for colors only
146
- const dirMatch = cls.match(/^border-([trbl])(?:-(.+))?$/);
162
+ const dirMatch = cls.match(/^border-([trblse])(?:-(.+))?$/);
147
163
  if (dirMatch) {
148
164
  const dir = dirMatch[1];
149
165
  const valueStr = dirMatch[2] || ""; // empty string for border-t
150
166
 
151
167
  // If it's a color pattern, let parseColor handle it
152
168
  // Try to parse as color - if it succeeds, return null (let parseColor handle it)
153
- if (valueStr) {
169
+ // Note: We skip color check for s/e since React Native doesn't support borderStartColor/borderEndColor
170
+ if (valueStr && dir !== "s" && dir !== "e") {
154
171
  const colorResult = parseColor(cls, customColors);
155
172
  if (colorResult !== null) {
156
173
  return null; // It's a color, let parseColor handle it
@@ -220,7 +237,7 @@ function parseBorderRadius(cls: string): StyleObject | null {
220
237
  return null;
221
238
  }
222
239
 
223
- // Specific corners: rounded-tl, rounded-tl-lg, rounded-tl-[8px]
240
+ // Specific physical corners: rounded-tl, rounded-tl-lg, rounded-tl-[8px]
224
241
  const cornerMatch = rest.match(/^(tl|tr|bl|br)(?:-(.+))?$/);
225
242
  if (cornerMatch) {
226
243
  const corner = cornerMatch[1];
@@ -244,8 +261,34 @@ function parseBorderRadius(cls: string): StyleObject | null {
244
261
  return null;
245
262
  }
246
263
 
247
- // Sides: rounded-t, rounded-t-lg, rounded-t-[8px]
248
- const sideMatch = rest.match(/^([trbl])(?:-(.+))?$/);
264
+ // Logical corners (RTL-aware): rounded-ss, rounded-se, rounded-es, rounded-ee
265
+ // ss = start-start (top-start), se = start-end (top-end)
266
+ // es = end-start (bottom-start), ee = end-end (bottom-end)
267
+ const logicalCornerMatch = rest.match(/^(ss|se|es|ee)(?:-(.+))?$/);
268
+ if (logicalCornerMatch) {
269
+ const corner = logicalCornerMatch[1];
270
+ const valueStr = logicalCornerMatch[2] || ""; // empty string for rounded-ss
271
+
272
+ // Try arbitrary value first
273
+ if (valueStr.startsWith("[")) {
274
+ const arbitraryValue = parseArbitraryBorderRadius(valueStr);
275
+ if (arbitraryValue !== null) {
276
+ return { [BORDER_RADIUS_LOGICAL_CORNER_MAP[corner]]: arbitraryValue };
277
+ }
278
+ return null;
279
+ }
280
+
281
+ // Try preset scale
282
+ const scaleValue = BORDER_RADIUS_SCALE[valueStr];
283
+ if (scaleValue !== undefined) {
284
+ return { [BORDER_RADIUS_LOGICAL_CORNER_MAP[corner]]: scaleValue };
285
+ }
286
+
287
+ return null;
288
+ }
289
+
290
+ // Sides: rounded-t, rounded-t-lg, rounded-t-[8px], rounded-s, rounded-e (RTL-aware)
291
+ const sideMatch = rest.match(/^([trblse])(?:-(.+))?$/);
249
292
  if (sideMatch) {
250
293
  const side = sideMatch[1];
251
294
  const valueStr = sideMatch[2] || ""; // empty string for rounded-t
@@ -97,6 +97,7 @@ export {
97
97
  hasModifier,
98
98
  isColorClass,
99
99
  isColorSchemeModifier,
100
+ isDirectionalModifier,
100
101
  isPlatformModifier,
101
102
  isSchemeModifier,
102
103
  isStateModifier,
@@ -105,6 +106,7 @@ export {
105
106
  } from "./modifiers";
106
107
  export type {
107
108
  ColorSchemeModifierType,
109
+ DirectionalModifierType,
108
110
  ModifierType,
109
111
  ParsedModifier,
110
112
  PlatformModifierType,
@@ -645,3 +645,77 @@ describe("parseLayout - specific property coverage", () => {
645
645
  expect(insetY).not.toHaveProperty("right");
646
646
  });
647
647
  });
648
+
649
+ describe("parseLayout - logical positioning (RTL-aware)", () => {
650
+ it("should parse start positioning with preset values", () => {
651
+ expect(parseLayout("start-0")).toEqual({ start: 0 });
652
+ expect(parseLayout("start-4")).toEqual({ start: 16 });
653
+ expect(parseLayout("start-8")).toEqual({ start: 32 });
654
+ expect(parseLayout("start-0.5")).toEqual({ start: 2 });
655
+ expect(parseLayout("start-2.5")).toEqual({ start: 10 });
656
+ });
657
+
658
+ it("should parse end positioning with preset values", () => {
659
+ expect(parseLayout("end-0")).toEqual({ end: 0 });
660
+ expect(parseLayout("end-4")).toEqual({ end: 16 });
661
+ expect(parseLayout("end-8")).toEqual({ end: 32 });
662
+ expect(parseLayout("end-0.5")).toEqual({ end: 2 });
663
+ expect(parseLayout("end-2.5")).toEqual({ end: 10 });
664
+ });
665
+
666
+ it("should parse start/end with arbitrary pixel values", () => {
667
+ expect(parseLayout("start-[10px]")).toEqual({ start: 10 });
668
+ expect(parseLayout("start-[50]")).toEqual({ start: 50 });
669
+ expect(parseLayout("end-[10px]")).toEqual({ end: 10 });
670
+ expect(parseLayout("end-[50]")).toEqual({ end: 50 });
671
+ });
672
+
673
+ it("should parse start/end with arbitrary percentage values", () => {
674
+ expect(parseLayout("start-[10%]")).toEqual({ start: "10%" });
675
+ expect(parseLayout("start-[50%]")).toEqual({ start: "50%" });
676
+ expect(parseLayout("end-[10%]")).toEqual({ end: "10%" });
677
+ expect(parseLayout("end-[50%]")).toEqual({ end: "50%" });
678
+ });
679
+
680
+ it("should parse negative start/end with preset values", () => {
681
+ expect(parseLayout("-start-4")).toEqual({ start: -16 });
682
+ expect(parseLayout("-start-8")).toEqual({ start: -32 });
683
+ expect(parseLayout("-end-4")).toEqual({ end: -16 });
684
+ expect(parseLayout("-end-8")).toEqual({ end: -32 });
685
+ });
686
+
687
+ it("should parse negative start/end with arbitrary values", () => {
688
+ expect(parseLayout("-start-[10px]")).toEqual({ start: -10 });
689
+ expect(parseLayout("-start-[50]")).toEqual({ start: -50 });
690
+ expect(parseLayout("-end-[10px]")).toEqual({ end: -10 });
691
+ expect(parseLayout("-end-[50]")).toEqual({ end: -50 });
692
+ expect(parseLayout("-start-[10%]")).toEqual({ start: "-10%" });
693
+ expect(parseLayout("-end-[25%]")).toEqual({ end: "-25%" });
694
+ });
695
+
696
+ it("should handle auto value for start/end", () => {
697
+ expect(parseLayout("start-auto")).toEqual({});
698
+ expect(parseLayout("end-auto")).toEqual({});
699
+ });
700
+ });
701
+
702
+ describe("parseLayout - logical inset (RTL-aware)", () => {
703
+ it("should parse inset-s (start) with preset values", () => {
704
+ expect(parseLayout("inset-s-0")).toEqual({ start: 0 });
705
+ expect(parseLayout("inset-s-4")).toEqual({ start: 16 });
706
+ expect(parseLayout("inset-s-8")).toEqual({ start: 32 });
707
+ });
708
+
709
+ it("should parse inset-e (end) with preset values", () => {
710
+ expect(parseLayout("inset-e-0")).toEqual({ end: 0 });
711
+ expect(parseLayout("inset-e-4")).toEqual({ end: 16 });
712
+ expect(parseLayout("inset-e-8")).toEqual({ end: 32 });
713
+ });
714
+
715
+ it("should parse inset-s/inset-e with arbitrary values", () => {
716
+ expect(parseLayout("inset-s-[10px]")).toEqual({ start: 10 });
717
+ expect(parseLayout("inset-s-[20%]")).toEqual({ start: "20%" });
718
+ expect(parseLayout("inset-e-[10px]")).toEqual({ end: 10 });
719
+ expect(parseLayout("inset-e-[20%]")).toEqual({ end: "20%" });
720
+ });
721
+ });
@@ -339,6 +339,68 @@ export function parseLayout(cls: string): StyleObject | null {
339
339
  }
340
340
  }
341
341
 
342
+ // Start positioning (RTL-aware): start-0, start-4, start-[10px], -start-4, etc.
343
+ const startMatch = cls.match(/^(-?)start-(.+)$/);
344
+ if (startMatch) {
345
+ const [, negPrefix, startKey] = startMatch;
346
+ const isNegative = negPrefix === "-";
347
+
348
+ // Auto value - return empty object (no-op, removes the property)
349
+ if (startKey === "auto") {
350
+ return {};
351
+ }
352
+
353
+ // Arbitrary values: start-[123px], start-[50%], -start-[10px]
354
+ const arbitraryStart = parseArbitraryInset(startKey);
355
+ if (arbitraryStart !== null) {
356
+ if (typeof arbitraryStart === "number") {
357
+ return { start: isNegative ? -arbitraryStart : arbitraryStart };
358
+ }
359
+ // Percentage values with negative prefix
360
+ if (isNegative && arbitraryStart.endsWith("%")) {
361
+ const numValue = parseFloat(arbitraryStart);
362
+ return { start: `${-numValue}%` };
363
+ }
364
+ return { start: arbitraryStart };
365
+ }
366
+
367
+ const startValue = INSET_SCALE[startKey];
368
+ if (startValue !== undefined) {
369
+ return { start: isNegative ? -startValue : startValue };
370
+ }
371
+ }
372
+
373
+ // End positioning (RTL-aware): end-0, end-4, end-[10px], -end-4, etc.
374
+ const endMatch = cls.match(/^(-?)end-(.+)$/);
375
+ if (endMatch) {
376
+ const [, negPrefix, endKey] = endMatch;
377
+ const isNegative = negPrefix === "-";
378
+
379
+ // Auto value - return empty object (no-op, removes the property)
380
+ if (endKey === "auto") {
381
+ return {};
382
+ }
383
+
384
+ // Arbitrary values: end-[123px], end-[50%], -end-[10px]
385
+ const arbitraryEnd = parseArbitraryInset(endKey);
386
+ if (arbitraryEnd !== null) {
387
+ if (typeof arbitraryEnd === "number") {
388
+ return { end: isNegative ? -arbitraryEnd : arbitraryEnd };
389
+ }
390
+ // Percentage values with negative prefix
391
+ if (isNegative && arbitraryEnd.endsWith("%")) {
392
+ const numValue = parseFloat(arbitraryEnd);
393
+ return { end: `${-numValue}%` };
394
+ }
395
+ return { end: arbitraryEnd };
396
+ }
397
+
398
+ const endValue = INSET_SCALE[endKey];
399
+ if (endValue !== undefined) {
400
+ return { end: isNegative ? -endValue : endValue };
401
+ }
402
+ }
403
+
342
404
  // Inset X (left and right): inset-x-0, inset-x-4, inset-x-[10px], etc.
343
405
  if (cls.startsWith("inset-x-")) {
344
406
  const insetKey = cls.substring(8);
@@ -371,6 +433,38 @@ export function parseLayout(cls: string): StyleObject | null {
371
433
  }
372
434
  }
373
435
 
436
+ // Inset S (start, RTL-aware): inset-s-0, inset-s-4, inset-s-[10px], etc.
437
+ if (cls.startsWith("inset-s-")) {
438
+ const insetKey = cls.substring(8);
439
+
440
+ // Arbitrary values: inset-s-[123px], inset-s-[50%]
441
+ const arbitraryInset = parseArbitraryInset(insetKey);
442
+ if (arbitraryInset !== null) {
443
+ return { start: arbitraryInset };
444
+ }
445
+
446
+ const insetValue = INSET_SCALE[insetKey];
447
+ if (insetValue !== undefined) {
448
+ return { start: insetValue };
449
+ }
450
+ }
451
+
452
+ // Inset E (end, RTL-aware): inset-e-0, inset-e-4, inset-e-[10px], etc.
453
+ if (cls.startsWith("inset-e-")) {
454
+ const insetKey = cls.substring(8);
455
+
456
+ // Arbitrary values: inset-e-[123px], inset-e-[50%]
457
+ const arbitraryInset = parseArbitraryInset(insetKey);
458
+ if (arbitraryInset !== null) {
459
+ return { end: arbitraryInset };
460
+ }
461
+
462
+ const insetValue = INSET_SCALE[insetKey];
463
+ if (insetValue !== undefined) {
464
+ return { end: insetValue };
465
+ }
466
+ }
467
+
374
468
  // Inset (all sides): inset-0, inset-4, inset-[10px], etc.
375
469
  if (cls.startsWith("inset-")) {
376
470
  const insetKey = cls.substring(6);
@@ -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);