@mgcrea/react-native-tailwind 0.8.0 → 0.9.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 (50) hide show
  1. package/README.md +152 -0
  2. package/dist/babel/config-loader.ts +2 -0
  3. package/dist/babel/index.cjs +178 -5
  4. package/dist/babel/plugin.d.ts +2 -0
  5. package/dist/babel/plugin.test.ts +241 -0
  6. package/dist/babel/plugin.ts +187 -10
  7. package/dist/babel/utils/attributeMatchers.test.ts +294 -0
  8. package/dist/babel/utils/componentSupport.test.ts +426 -0
  9. package/dist/babel/utils/platformModifierProcessing.d.ts +30 -0
  10. package/dist/babel/utils/platformModifierProcessing.ts +80 -0
  11. package/dist/babel/utils/styleInjection.d.ts +4 -0
  12. package/dist/babel/utils/styleInjection.ts +28 -0
  13. package/dist/babel/utils/styleTransforms.ts +1 -0
  14. package/dist/parser/colors.test.js +1 -1
  15. package/dist/parser/index.d.ts +2 -2
  16. package/dist/parser/index.js +1 -1
  17. package/dist/parser/modifiers.d.ts +20 -2
  18. package/dist/parser/modifiers.js +1 -1
  19. package/dist/runtime.cjs +1 -1
  20. package/dist/runtime.cjs.map +4 -4
  21. package/dist/runtime.js +1 -1
  22. package/dist/runtime.js.map +4 -4
  23. package/dist/stubs/tw.test.js +1 -0
  24. package/dist/utils/flattenColors.d.ts +1 -0
  25. package/dist/utils/flattenColors.js +1 -1
  26. package/dist/utils/flattenColors.test.js +1 -1
  27. package/package.json +6 -5
  28. package/src/babel/config-loader.ts +2 -0
  29. package/src/babel/plugin.test.ts +241 -0
  30. package/src/babel/plugin.ts +187 -10
  31. package/src/babel/utils/attributeMatchers.test.ts +294 -0
  32. package/src/babel/utils/componentSupport.test.ts +426 -0
  33. package/src/babel/utils/platformModifierProcessing.ts +80 -0
  34. package/src/babel/utils/styleInjection.ts +28 -0
  35. package/src/babel/utils/styleTransforms.ts +1 -0
  36. package/src/parser/aspectRatio.ts +1 -0
  37. package/src/parser/borders.ts +2 -0
  38. package/src/parser/colors.test.ts +32 -0
  39. package/src/parser/colors.ts +2 -0
  40. package/src/parser/index.ts +10 -3
  41. package/src/parser/layout.ts +2 -0
  42. package/src/parser/modifiers.ts +38 -4
  43. package/src/parser/placeholder.ts +1 -0
  44. package/src/parser/sizing.ts +1 -0
  45. package/src/parser/spacing.ts +1 -0
  46. package/src/parser/transforms.ts +5 -0
  47. package/src/parser/typography.ts +2 -0
  48. package/src/stubs/tw.test.ts +27 -0
  49. package/src/utils/flattenColors.test.ts +100 -0
  50. package/src/utils/flattenColors.ts +3 -1
@@ -1,8 +1,12 @@
1
1
  /**
2
- * Modifier parsing utilities for state-based class names (active:, hover:, focus:, placeholder:)
2
+ * Modifier parsing utilities for state-based and platform-specific class names
3
+ * - State modifiers: active:, hover:, focus:, disabled:, placeholder:
4
+ * - Platform modifiers: ios:, android:, web:
3
5
  */
4
6
 
5
- export type ModifierType = "active" | "hover" | "focus" | "disabled" | "placeholder";
7
+ export type StateModifierType = "active" | "hover" | "focus" | "disabled" | "placeholder";
8
+ export type PlatformModifierType = "ios" | "android" | "web";
9
+ export type ModifierType = StateModifierType | PlatformModifierType;
6
10
 
7
11
  export type ParsedModifier = {
8
12
  modifier: ModifierType;
@@ -10,9 +14,9 @@ export type ParsedModifier = {
10
14
  };
11
15
 
12
16
  /**
13
- * Supported modifiers that map to component states or pseudo-elements
17
+ * Supported state modifiers that map to component states or pseudo-elements
14
18
  */
15
- const SUPPORTED_MODIFIERS: readonly ModifierType[] = [
19
+ const STATE_MODIFIERS: readonly StateModifierType[] = [
16
20
  "active",
17
21
  "hover",
18
22
  "focus",
@@ -20,6 +24,16 @@ const SUPPORTED_MODIFIERS: readonly ModifierType[] = [
20
24
  "placeholder",
21
25
  ] as const;
22
26
 
27
+ /**
28
+ * Supported platform modifiers that map to Platform.OS values
29
+ */
30
+ const PLATFORM_MODIFIERS: readonly PlatformModifierType[] = ["ios", "android", "web"] as const;
31
+
32
+ /**
33
+ * All supported modifiers (state + platform)
34
+ */
35
+ const SUPPORTED_MODIFIERS: readonly ModifierType[] = [...STATE_MODIFIERS, ...PLATFORM_MODIFIERS] as const;
36
+
23
37
  /**
24
38
  * Parse a class name to detect and extract modifiers
25
39
  *
@@ -73,6 +87,26 @@ export function hasModifier(cls: string): boolean {
73
87
  return parseModifier(cls) !== null;
74
88
  }
75
89
 
90
+ /**
91
+ * Check if a modifier is a state modifier (active, hover, focus, disabled, placeholder)
92
+ *
93
+ * @param modifier - Modifier type to check
94
+ * @returns true if modifier is a state modifier
95
+ */
96
+ export function isStateModifier(modifier: ModifierType): modifier is StateModifierType {
97
+ return STATE_MODIFIERS.includes(modifier as StateModifierType);
98
+ }
99
+
100
+ /**
101
+ * Check if a modifier is a platform modifier (ios, android, web)
102
+ *
103
+ * @param modifier - Modifier type to check
104
+ * @returns true if modifier is a platform modifier
105
+ */
106
+ export function isPlatformModifier(modifier: ModifierType): modifier is PlatformModifierType {
107
+ return PLATFORM_MODIFIERS.includes(modifier as PlatformModifierType);
108
+ }
109
+
76
110
  /**
77
111
  * Split a space-separated className string into base and modifier classes
78
112
  *
@@ -27,6 +27,7 @@ export function parsePlaceholderClass(cls: string, customColors?: Record<string,
27
27
  // Check if it's a text color class
28
28
  if (!cls.startsWith("text-")) {
29
29
  // Warn about unsupported utilities
30
+ /* v8 ignore next 5 */
30
31
  if (process.env.NODE_ENV !== "production") {
31
32
  console.warn(
32
33
  `[react-native-tailwind] Only text color utilities are supported in placeholder: modifier. ` +
@@ -80,6 +80,7 @@ function parseArbitrarySize(value: string): number | string | null {
80
80
 
81
81
  // Unsupported units (rem, em, vh, vw, etc.) - warn and reject
82
82
  if (value.startsWith("[") && value.endsWith("]")) {
83
+ /* v8 ignore next 5 */
83
84
  if (process.env.NODE_ENV !== "production") {
84
85
  console.warn(
85
86
  `[react-native-tailwind] Unsupported arbitrary size unit: ${value}. Only px and % are supported.`,
@@ -55,6 +55,7 @@ function parseArbitrarySpacing(value: string): number | null {
55
55
 
56
56
  // Warn about unsupported formats
57
57
  if (value.startsWith("[") && value.endsWith("]")) {
58
+ /* v8 ignore next 5 */
58
59
  if (process.env.NODE_ENV !== "production") {
59
60
  console.warn(
60
61
  `[react-native-tailwind] Unsupported arbitrary spacing value: ${value}. Only px values are supported (e.g., [16px] or [16]).`,
@@ -70,6 +70,7 @@ function parseArbitraryScale(value: string): number | null {
70
70
 
71
71
  // Unsupported format
72
72
  if (value.startsWith("[") && value.endsWith("]")) {
73
+ /* v8 ignore next 5 */
73
74
  if (process.env.NODE_ENV !== "production") {
74
75
  console.warn(
75
76
  `[react-native-tailwind] Invalid arbitrary scale value: ${value}. Only numbers are supported (e.g., [1.5], [0.75]).`,
@@ -93,6 +94,7 @@ function parseArbitraryRotation(value: string): string | null {
93
94
 
94
95
  // Unsupported format
95
96
  if (value.startsWith("[") && value.endsWith("]")) {
97
+ /* v8 ignore next 5 */
96
98
  if (process.env.NODE_ENV !== "production") {
97
99
  console.warn(
98
100
  `[react-native-tailwind] Invalid arbitrary rotation value: ${value}. Only deg unit is supported (e.g., [45deg], [-15deg]).`,
@@ -123,6 +125,7 @@ function parseArbitraryTranslation(value: string): number | string | null {
123
125
 
124
126
  // Unsupported units
125
127
  if (value.startsWith("[") && value.endsWith("]")) {
128
+ /* v8 ignore next 5 */
126
129
  if (process.env.NODE_ENV !== "production") {
127
130
  console.warn(
128
131
  `[react-native-tailwind] Unsupported arbitrary translation unit: ${value}. Only px and % are supported.`,
@@ -146,6 +149,7 @@ function parseArbitraryPerspective(value: string): number | null {
146
149
 
147
150
  // Unsupported format
148
151
  if (value.startsWith("[") && value.endsWith("]")) {
152
+ /* v8 ignore next 5 */
149
153
  if (process.env.NODE_ENV !== "production") {
150
154
  console.warn(
151
155
  `[react-native-tailwind] Invalid arbitrary perspective value: ${value}. Only integers are supported (e.g., [1500]).`,
@@ -164,6 +168,7 @@ function parseArbitraryPerspective(value: string): number | null {
164
168
  export function parseTransform(cls: string): StyleObject | null {
165
169
  // Transform origin warning (not supported in React Native)
166
170
  if (cls.startsWith("origin-")) {
171
+ /* v8 ignore next 5 */
167
172
  if (process.env.NODE_ENV !== "production") {
168
173
  console.warn(
169
174
  `[react-native-tailwind] transform-origin is not supported in React Native. Class "${cls}" will be ignored.`,
@@ -125,6 +125,7 @@ function parseArbitraryFontSize(value: string): number | null {
125
125
 
126
126
  // Warn about unsupported formats
127
127
  if (value.startsWith("[") && value.endsWith("]")) {
128
+ /* v8 ignore next 5 */
128
129
  if (process.env.NODE_ENV !== "production") {
129
130
  console.warn(
130
131
  `[react-native-tailwind] Unsupported arbitrary font size value: ${value}. Only px values are supported (e.g., [18px] or [18]).`,
@@ -149,6 +150,7 @@ function parseArbitraryLineHeight(value: string): number | null {
149
150
 
150
151
  // Warn about unsupported formats
151
152
  if (value.startsWith("[") && value.endsWith("]")) {
153
+ /* v8 ignore next 5 */
152
154
  if (process.env.NODE_ENV !== "production") {
153
155
  console.warn(
154
156
  `[react-native-tailwind] Unsupported arbitrary line height value: ${value}. Only px values are supported (e.g., [24px] or [24]).`,
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { tw, twStyle } from "./tw";
3
+
4
+ describe("tw stub", () => {
5
+ it("should throw error when tw() is called without Babel transformation", () => {
6
+ expect(() => tw`bg-blue-500`).toThrow(
7
+ "tw() must be transformed by the Babel plugin. " +
8
+ "Ensure @mgcrea/react-native-tailwind/babel is configured in your babel.config.js. " +
9
+ "For runtime parsing, use: import { tw } from '@mgcrea/react-native-tailwind/runtime'",
10
+ );
11
+ });
12
+
13
+ it("should throw error when twStyle() is called without Babel transformation", () => {
14
+ expect(() => twStyle("bg-blue-500")).toThrow(
15
+ "twStyle() must be transformed by the Babel plugin. " +
16
+ "Ensure @mgcrea/react-native-tailwind/babel is configured in your babel.config.js. " +
17
+ "For runtime parsing, use: import { twStyle } from '@mgcrea/react-native-tailwind/runtime'",
18
+ );
19
+ });
20
+
21
+ it("should throw error with template literal interpolation", () => {
22
+ const dynamic = "active";
23
+ expect(() => tw`bg-blue-500 ${dynamic}:bg-blue-700`).toThrow(
24
+ "tw() must be transformed by the Babel plugin",
25
+ );
26
+ });
27
+ });
@@ -358,4 +358,104 @@ describe("flattenColors", () => {
358
358
 
359
359
  expect(keys).toEqual(["z", "a", "m"]);
360
360
  });
361
+
362
+ it("should handle DEFAULT key in color scale objects", () => {
363
+ const colors = {
364
+ primary: {
365
+ "50": "#eefdfd",
366
+ "100": "#d4f9f9",
367
+ "200": "#aef2f3",
368
+ "500": "#1bacb5",
369
+ "900": "#1e4f5b",
370
+ DEFAULT: "#1bacb5",
371
+ },
372
+ };
373
+
374
+ expect(flattenColors(colors)).toEqual({
375
+ primary: "#1bacb5", // DEFAULT becomes the parent key
376
+ "primary-50": "#eefdfd",
377
+ "primary-100": "#d4f9f9",
378
+ "primary-200": "#aef2f3",
379
+ "primary-500": "#1bacb5",
380
+ "primary-900": "#1e4f5b",
381
+ });
382
+ });
383
+
384
+ it("should handle DEFAULT key with multiple color scales", () => {
385
+ const colors = {
386
+ primary: {
387
+ DEFAULT: "#1bacb5",
388
+ "500": "#1bacb5",
389
+ },
390
+ secondary: {
391
+ DEFAULT: "#ff6b6b",
392
+ "500": "#ff6b6b",
393
+ },
394
+ };
395
+
396
+ expect(flattenColors(colors)).toEqual({
397
+ primary: "#1bacb5",
398
+ "primary-500": "#1bacb5",
399
+ secondary: "#ff6b6b",
400
+ "secondary-500": "#ff6b6b",
401
+ });
402
+ });
403
+
404
+ it("should handle DEFAULT key in nested structures", () => {
405
+ const colors = {
406
+ brand: {
407
+ primary: {
408
+ DEFAULT: "#1bacb5",
409
+ light: "#d4f9f9",
410
+ dark: "#0e343e",
411
+ },
412
+ },
413
+ };
414
+
415
+ expect(flattenColors(colors)).toEqual({
416
+ "brand-primary": "#1bacb5", // DEFAULT uses parent key
417
+ "brand-primary-light": "#d4f9f9",
418
+ "brand-primary-dark": "#0e343e",
419
+ });
420
+ });
421
+
422
+ it("should handle DEFAULT at top level (edge case)", () => {
423
+ const colors = {
424
+ DEFAULT: "#000000",
425
+ primary: "#ff0000",
426
+ };
427
+
428
+ expect(flattenColors(colors)).toEqual({
429
+ DEFAULT: "#000000", // Top-level DEFAULT kept as-is (no parent)
430
+ primary: "#ff0000",
431
+ });
432
+ });
433
+
434
+ it("should handle mixed DEFAULT and regular keys", () => {
435
+ const colors = {
436
+ gray: {
437
+ "50": "#f9fafb",
438
+ "100": "#f3f4f6",
439
+ DEFAULT: "#6b7280",
440
+ "500": "#6b7280",
441
+ "900": "#111827",
442
+ },
443
+ white: "#ffffff",
444
+ brand: {
445
+ DEFAULT: "#ff6b6b",
446
+ accent: "#4ecdc4",
447
+ },
448
+ };
449
+
450
+ expect(flattenColors(colors)).toEqual({
451
+ "gray-50": "#f9fafb",
452
+ "gray-100": "#f3f4f6",
453
+ gray: "#6b7280", // DEFAULT becomes parent key
454
+ "gray-500": "#6b7280",
455
+ "gray-900": "#111827",
456
+ white: "#ffffff",
457
+ brand: "#ff6b6b", // DEFAULT becomes parent key
458
+ "brand-accent": "#4ecdc4",
459
+ });
460
+ });
361
461
  });
@@ -8,6 +8,7 @@ type NestedColors = {
8
8
  /**
9
9
  * Flatten nested color objects into flat key-value map
10
10
  * Example: { brand: { light: '#fff', dark: '#000' } } => { 'brand-light': '#fff', 'brand-dark': '#000' }
11
+ * Special handling for DEFAULT: { primary: { DEFAULT: '#000', 500: '#333' } } => { 'primary': '#000', 'primary-500': '#333' }
11
12
  *
12
13
  * @param colors - Nested color object where values can be strings or objects
13
14
  * @param prefix - Optional prefix for nested keys (used for recursion)
@@ -17,7 +18,8 @@ export function flattenColors(colors: NestedColors, prefix = ""): Record<string,
17
18
  const result: Record<string, string> = {};
18
19
 
19
20
  for (const [key, value] of Object.entries(colors)) {
20
- const newKey = prefix ? `${prefix}-${key}` : key;
21
+ // Special handling for DEFAULT key - use parent key without suffix
22
+ const newKey = key === "DEFAULT" && prefix ? prefix : prefix ? `${prefix}-${key}` : key;
21
23
 
22
24
  if (typeof value === "string") {
23
25
  result[newKey] = value;