@mgcrea/react-native-tailwind 0.5.2 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,406 @@
1
+ /**
2
+ * Transform utilities (scale, rotate, translate, skew, perspective)
3
+ */
4
+
5
+ import type { StyleObject } from "../types";
6
+ import { SPACING_SCALE } from "./spacing";
7
+
8
+ // Scale values (percentage to decimal)
9
+ export const SCALE_MAP: Record<string, number> = {
10
+ 0: 0,
11
+ 50: 0.5,
12
+ 75: 0.75,
13
+ 90: 0.9,
14
+ 95: 0.95,
15
+ 100: 1,
16
+ 105: 1.05,
17
+ 110: 1.1,
18
+ 125: 1.25,
19
+ 150: 1.5,
20
+ 200: 2,
21
+ };
22
+
23
+ // Rotation degrees
24
+ export const ROTATE_MAP: Record<string, number> = {
25
+ 0: 0,
26
+ 1: 1,
27
+ 2: 2,
28
+ 3: 3,
29
+ 6: 6,
30
+ 12: 12,
31
+ 45: 45,
32
+ 90: 90,
33
+ 180: 180,
34
+ };
35
+
36
+ // Skew degrees
37
+ export const SKEW_MAP: Record<string, number> = {
38
+ 0: 0,
39
+ 1: 1,
40
+ 2: 2,
41
+ 3: 3,
42
+ 6: 6,
43
+ 12: 12,
44
+ };
45
+
46
+ // Perspective values
47
+ export const PERSPECTIVE_SCALE: Record<string, number> = {
48
+ 0: 0,
49
+ 100: 100,
50
+ 200: 200,
51
+ 300: 300,
52
+ 400: 400,
53
+ 500: 500,
54
+ 600: 600,
55
+ 700: 700,
56
+ 800: 800,
57
+ 900: 900,
58
+ 1000: 1000,
59
+ };
60
+
61
+ /**
62
+ * Parse arbitrary scale value: [1.23], [0.5]
63
+ * Returns number for valid scale, null otherwise
64
+ */
65
+ function parseArbitraryScale(value: string): number | null {
66
+ const scaleMatch = value.match(/^\[(-?\d+(?:\.\d+)?)\]$/);
67
+ if (scaleMatch) {
68
+ return parseFloat(scaleMatch[1]);
69
+ }
70
+
71
+ // Unsupported format
72
+ if (value.startsWith("[") && value.endsWith("]")) {
73
+ if (process.env.NODE_ENV !== "production") {
74
+ console.warn(
75
+ `[react-native-tailwind] Invalid arbitrary scale value: ${value}. Only numbers are supported (e.g., [1.5], [0.75]).`,
76
+ );
77
+ }
78
+ return null;
79
+ }
80
+
81
+ return null;
82
+ }
83
+
84
+ /**
85
+ * Parse arbitrary rotation value: [37deg], [-15deg]
86
+ * Returns string for valid rotation, null otherwise
87
+ */
88
+ function parseArbitraryRotation(value: string): string | null {
89
+ const rotateMatch = value.match(/^\[(-?\d+(?:\.\d+)?)deg\]$/);
90
+ if (rotateMatch) {
91
+ return `${rotateMatch[1]}deg`;
92
+ }
93
+
94
+ // Unsupported format
95
+ if (value.startsWith("[") && value.endsWith("]")) {
96
+ if (process.env.NODE_ENV !== "production") {
97
+ console.warn(
98
+ `[react-native-tailwind] Invalid arbitrary rotation value: ${value}. Only deg unit is supported (e.g., [45deg], [-15deg]).`,
99
+ );
100
+ }
101
+ return null;
102
+ }
103
+
104
+ return null;
105
+ }
106
+
107
+ /**
108
+ * Parse arbitrary translation value: [123px], [123], [50%], [-10px]
109
+ * Returns number for px values, string for % values, null for unsupported units
110
+ */
111
+ function parseArbitraryTranslation(value: string): number | string | null {
112
+ // Match: [123px], [123], [-123px], [-123] (pixels)
113
+ const pxMatch = value.match(/^\[(-?\d+)(?:px)?\]$/);
114
+ if (pxMatch) {
115
+ return parseInt(pxMatch[1], 10);
116
+ }
117
+
118
+ // Match: [50%], [-50%] (percentage)
119
+ const percentMatch = value.match(/^\[(-?\d+(?:\.\d+)?)%\]$/);
120
+ if (percentMatch) {
121
+ return `${percentMatch[1]}%`;
122
+ }
123
+
124
+ // Unsupported units
125
+ if (value.startsWith("[") && value.endsWith("]")) {
126
+ if (process.env.NODE_ENV !== "production") {
127
+ console.warn(
128
+ `[react-native-tailwind] Unsupported arbitrary translation unit: ${value}. Only px and % are supported.`,
129
+ );
130
+ }
131
+ return null;
132
+ }
133
+
134
+ return null;
135
+ }
136
+
137
+ /**
138
+ * Parse arbitrary perspective value: [1500], [2000]
139
+ * Returns number for valid perspective, null otherwise
140
+ */
141
+ function parseArbitraryPerspective(value: string): number | null {
142
+ const perspectiveMatch = value.match(/^\[(-?\d+)\]$/);
143
+ if (perspectiveMatch) {
144
+ return parseInt(perspectiveMatch[1], 10);
145
+ }
146
+
147
+ // Unsupported format
148
+ if (value.startsWith("[") && value.endsWith("]")) {
149
+ if (process.env.NODE_ENV !== "production") {
150
+ console.warn(
151
+ `[react-native-tailwind] Invalid arbitrary perspective value: ${value}. Only integers are supported (e.g., [1500]).`,
152
+ );
153
+ }
154
+ return null;
155
+ }
156
+
157
+ return null;
158
+ }
159
+
160
+ /**
161
+ * Parse transform classes
162
+ * Each transform class returns a transform array with a single transform object
163
+ */
164
+ export function parseTransform(cls: string): StyleObject | null {
165
+ // Transform origin warning (not supported in React Native)
166
+ if (cls.startsWith("origin-")) {
167
+ if (process.env.NODE_ENV !== "production") {
168
+ console.warn(
169
+ `[react-native-tailwind] transform-origin is not supported in React Native. Class "${cls}" will be ignored.`,
170
+ );
171
+ }
172
+ return null;
173
+ }
174
+
175
+ // Scale: scale-{value}
176
+ if (cls.startsWith("scale-")) {
177
+ const scaleKey = cls.substring(6);
178
+
179
+ // Arbitrary values: scale-[1.23]
180
+ const arbitraryScale = parseArbitraryScale(scaleKey);
181
+ if (arbitraryScale !== null) {
182
+ return { transform: [{ scale: arbitraryScale }] };
183
+ }
184
+
185
+ const scaleValue = SCALE_MAP[scaleKey];
186
+ if (scaleValue !== undefined) {
187
+ return { transform: [{ scale: scaleValue }] };
188
+ }
189
+ }
190
+
191
+ // Scale X: scale-x-{value}
192
+ if (cls.startsWith("scale-x-")) {
193
+ const scaleKey = cls.substring(8);
194
+
195
+ // Arbitrary values: scale-x-[1.5]
196
+ const arbitraryScale = parseArbitraryScale(scaleKey);
197
+ if (arbitraryScale !== null) {
198
+ return { transform: [{ scaleX: arbitraryScale }] };
199
+ }
200
+
201
+ const scaleValue = SCALE_MAP[scaleKey];
202
+ if (scaleValue !== undefined) {
203
+ return { transform: [{ scaleX: scaleValue }] };
204
+ }
205
+ }
206
+
207
+ // Scale Y: scale-y-{value}
208
+ if (cls.startsWith("scale-y-")) {
209
+ const scaleKey = cls.substring(8);
210
+
211
+ // Arbitrary values: scale-y-[2.5]
212
+ const arbitraryScale = parseArbitraryScale(scaleKey);
213
+ if (arbitraryScale !== null) {
214
+ return { transform: [{ scaleY: arbitraryScale }] };
215
+ }
216
+
217
+ const scaleValue = SCALE_MAP[scaleKey];
218
+ if (scaleValue !== undefined) {
219
+ return { transform: [{ scaleY: scaleValue }] };
220
+ }
221
+ }
222
+
223
+ // Rotate: rotate-{degrees}, -rotate-{degrees}
224
+ if (cls.startsWith("rotate-") || cls.startsWith("-rotate-")) {
225
+ const isNegative = cls.startsWith("-");
226
+ const rotateKey = isNegative ? cls.substring(8) : cls.substring(7);
227
+
228
+ // Arbitrary values: rotate-[37deg], -rotate-[15deg]
229
+ const arbitraryRotate = parseArbitraryRotation(rotateKey);
230
+ if (arbitraryRotate !== null) {
231
+ const degrees = isNegative ? `-${arbitraryRotate}` : arbitraryRotate;
232
+ return { transform: [{ rotate: degrees }] };
233
+ }
234
+
235
+ const rotateValue = ROTATE_MAP[rotateKey];
236
+ if (rotateValue !== undefined) {
237
+ const degrees = isNegative ? -rotateValue : rotateValue;
238
+ return { transform: [{ rotate: `${degrees}deg` }] };
239
+ }
240
+ }
241
+
242
+ // Rotate X: rotate-x-{degrees}, -rotate-x-{degrees}
243
+ if (cls.startsWith("rotate-x-") || cls.startsWith("-rotate-x-")) {
244
+ const isNegative = cls.startsWith("-");
245
+ const rotateKey = isNegative ? cls.substring(10) : cls.substring(9);
246
+
247
+ // Arbitrary values
248
+ const arbitraryRotate = parseArbitraryRotation(rotateKey);
249
+ if (arbitraryRotate !== null) {
250
+ const degrees = isNegative ? `-${arbitraryRotate}` : arbitraryRotate;
251
+ return { transform: [{ rotateX: degrees }] };
252
+ }
253
+
254
+ const rotateValue = ROTATE_MAP[rotateKey];
255
+ if (rotateValue !== undefined) {
256
+ const degrees = isNegative ? -rotateValue : rotateValue;
257
+ return { transform: [{ rotateX: `${degrees}deg` }] };
258
+ }
259
+ }
260
+
261
+ // Rotate Y: rotate-y-{degrees}, -rotate-y-{degrees}
262
+ if (cls.startsWith("rotate-y-") || cls.startsWith("-rotate-y-")) {
263
+ const isNegative = cls.startsWith("-");
264
+ const rotateKey = isNegative ? cls.substring(10) : cls.substring(9);
265
+
266
+ // Arbitrary values
267
+ const arbitraryRotate = parseArbitraryRotation(rotateKey);
268
+ if (arbitraryRotate !== null) {
269
+ const degrees = isNegative ? `-${arbitraryRotate}` : arbitraryRotate;
270
+ return { transform: [{ rotateY: degrees }] };
271
+ }
272
+
273
+ const rotateValue = ROTATE_MAP[rotateKey];
274
+ if (rotateValue !== undefined) {
275
+ const degrees = isNegative ? -rotateValue : rotateValue;
276
+ return { transform: [{ rotateY: `${degrees}deg` }] };
277
+ }
278
+ }
279
+
280
+ // Rotate Z: rotate-z-{degrees}, -rotate-z-{degrees}
281
+ if (cls.startsWith("rotate-z-") || cls.startsWith("-rotate-z-")) {
282
+ const isNegative = cls.startsWith("-");
283
+ const rotateKey = isNegative ? cls.substring(10) : cls.substring(9);
284
+
285
+ // Arbitrary values
286
+ const arbitraryRotate = parseArbitraryRotation(rotateKey);
287
+ if (arbitraryRotate !== null) {
288
+ const degrees = isNegative ? `-${arbitraryRotate}` : arbitraryRotate;
289
+ return { transform: [{ rotateZ: degrees }] };
290
+ }
291
+
292
+ const rotateValue = ROTATE_MAP[rotateKey];
293
+ if (rotateValue !== undefined) {
294
+ const degrees = isNegative ? -rotateValue : rotateValue;
295
+ return { transform: [{ rotateZ: `${degrees}deg` }] };
296
+ }
297
+ }
298
+
299
+ // Translate X: translate-x-{spacing}, -translate-x-{spacing}
300
+ if (cls.startsWith("translate-x-") || cls.startsWith("-translate-x-")) {
301
+ const isNegative = cls.startsWith("-");
302
+ const translateKey = isNegative ? cls.substring(13) : cls.substring(12);
303
+
304
+ // Arbitrary values: translate-x-[123px], -translate-x-[10px]
305
+ const arbitraryTranslate = parseArbitraryTranslation(translateKey);
306
+ if (arbitraryTranslate !== null) {
307
+ const value =
308
+ typeof arbitraryTranslate === "number"
309
+ ? isNegative
310
+ ? -arbitraryTranslate
311
+ : arbitraryTranslate
312
+ : isNegative
313
+ ? `-${arbitraryTranslate}`
314
+ : arbitraryTranslate;
315
+ return { transform: [{ translateX: value }] };
316
+ }
317
+
318
+ const translateValue = SPACING_SCALE[translateKey];
319
+ if (translateValue !== undefined) {
320
+ const value = isNegative ? -translateValue : translateValue;
321
+ return { transform: [{ translateX: value }] };
322
+ }
323
+ }
324
+
325
+ // Translate Y: translate-y-{spacing}, -translate-y-{spacing}
326
+ if (cls.startsWith("translate-y-") || cls.startsWith("-translate-y-")) {
327
+ const isNegative = cls.startsWith("-");
328
+ const translateKey = isNegative ? cls.substring(13) : cls.substring(12);
329
+
330
+ // Arbitrary values: translate-y-[123px], -translate-y-[10px]
331
+ const arbitraryTranslate = parseArbitraryTranslation(translateKey);
332
+ if (arbitraryTranslate !== null) {
333
+ const value =
334
+ typeof arbitraryTranslate === "number"
335
+ ? isNegative
336
+ ? -arbitraryTranslate
337
+ : arbitraryTranslate
338
+ : isNegative
339
+ ? `-${arbitraryTranslate}`
340
+ : arbitraryTranslate;
341
+ return { transform: [{ translateY: value }] };
342
+ }
343
+
344
+ const translateValue = SPACING_SCALE[translateKey];
345
+ if (translateValue !== undefined) {
346
+ const value = isNegative ? -translateValue : translateValue;
347
+ return { transform: [{ translateY: value }] };
348
+ }
349
+ }
350
+
351
+ // Skew X: skew-x-{degrees}, -skew-x-{degrees}
352
+ if (cls.startsWith("skew-x-") || cls.startsWith("-skew-x-")) {
353
+ const isNegative = cls.startsWith("-");
354
+ const skewKey = isNegative ? cls.substring(8) : cls.substring(7);
355
+
356
+ // Arbitrary values
357
+ const arbitrarySkew = parseArbitraryRotation(skewKey);
358
+ if (arbitrarySkew !== null) {
359
+ const degrees = isNegative ? `-${arbitrarySkew}` : arbitrarySkew;
360
+ return { transform: [{ skewX: degrees }] };
361
+ }
362
+
363
+ const skewValue = SKEW_MAP[skewKey];
364
+ if (skewValue !== undefined) {
365
+ const degrees = isNegative ? -skewValue : skewValue;
366
+ return { transform: [{ skewX: `${degrees}deg` }] };
367
+ }
368
+ }
369
+
370
+ // Skew Y: skew-y-{degrees}, -skew-y-{degrees}
371
+ if (cls.startsWith("skew-y-") || cls.startsWith("-skew-y-")) {
372
+ const isNegative = cls.startsWith("-");
373
+ const skewKey = isNegative ? cls.substring(8) : cls.substring(7);
374
+
375
+ // Arbitrary values
376
+ const arbitrarySkew = parseArbitraryRotation(skewKey);
377
+ if (arbitrarySkew !== null) {
378
+ const degrees = isNegative ? `-${arbitrarySkew}` : arbitrarySkew;
379
+ return { transform: [{ skewY: degrees }] };
380
+ }
381
+
382
+ const skewValue = SKEW_MAP[skewKey];
383
+ if (skewValue !== undefined) {
384
+ const degrees = isNegative ? -skewValue : skewValue;
385
+ return { transform: [{ skewY: `${degrees}deg` }] };
386
+ }
387
+ }
388
+
389
+ // Perspective: perspective-{value}
390
+ if (cls.startsWith("perspective-")) {
391
+ const perspectiveKey = cls.substring(12);
392
+
393
+ // Arbitrary values: perspective-[1500]
394
+ const arbitraryPerspective = parseArbitraryPerspective(perspectiveKey);
395
+ if (arbitraryPerspective !== null) {
396
+ return { transform: [{ perspective: arbitraryPerspective }] };
397
+ }
398
+
399
+ const perspectiveValue = PERSPECTIVE_SCALE[perspectiveKey];
400
+ if (perspectiveValue !== undefined) {
401
+ return { transform: [{ perspective: perspectiveValue }] };
402
+ }
403
+ }
404
+
405
+ return null;
406
+ }
@@ -31,6 +31,8 @@ describe("parseTypography - font size", () => {
31
31
  });
32
32
 
33
33
  it("should parse font size with arbitrary pixel values", () => {
34
+ expect(parseTypography("text-[13px]")).toEqual({ fontSize: 13 });
35
+ expect(parseTypography("text-[13]")).toEqual({ fontSize: 13 });
34
36
  expect(parseTypography("text-[18px]")).toEqual({ fontSize: 18 });
35
37
  expect(parseTypography("text-[18]")).toEqual({ fontSize: 18 });
36
38
  expect(parseTypography("text-[22px]")).toEqual({ fontSize: 22 });
@@ -39,6 +41,14 @@ describe("parseTypography - font size", () => {
39
41
  });
40
42
  });
41
43
 
44
+ describe("parseTypography - font family", () => {
45
+ it("should parse font family values", () => {
46
+ expect(parseTypography("font-sans")).toEqual({ fontFamily: "System" });
47
+ expect(parseTypography("font-serif")).toEqual({ fontFamily: "serif" });
48
+ expect(parseTypography("font-mono")).toEqual({ fontFamily: "Courier" });
49
+ });
50
+ });
51
+
42
52
  describe("parseTypography - font weight", () => {
43
53
  it("should parse font weight values", () => {
44
54
  expect(parseTypography("font-thin")).toEqual({ fontWeight: "100" });
@@ -175,6 +185,8 @@ describe("parseTypography - comprehensive coverage", () => {
175
185
  it("should handle all typography categories independently", () => {
176
186
  // Font size
177
187
  expect(parseTypography("text-base")).toEqual({ fontSize: 16 });
188
+ // Font family
189
+ expect(parseTypography("font-mono")).toEqual({ fontFamily: "Courier" });
178
190
  // Font weight
179
191
  expect(parseTypography("font-bold")).toEqual({ fontWeight: "700" });
180
192
  // Font style
@@ -31,6 +31,13 @@ export const LETTER_SPACING_SCALE: Record<string, number> = {
31
31
  widest: 1.6,
32
32
  };
33
33
 
34
+ // Font family utilities
35
+ const FONT_FAMILY_MAP: Record<string, StyleObject> = {
36
+ "font-sans": { fontFamily: "System" },
37
+ "font-serif": { fontFamily: "serif" },
38
+ "font-mono": { fontFamily: "Courier" },
39
+ };
40
+
34
41
  // Font weight utilities
35
42
  const FONT_WEIGHT_MAP: Record<string, StyleObject> = {
36
43
  "font-thin": { fontWeight: "100" },
@@ -175,6 +182,7 @@ export function parseTypography(cls: string): StyleObject | null {
175
182
 
176
183
  // Try each lookup table in order
177
184
  return (
185
+ FONT_FAMILY_MAP[cls] ??
178
186
  FONT_WEIGHT_MAP[cls] ??
179
187
  FONT_STYLE_MAP[cls] ??
180
188
  TEXT_ALIGN_MAP[cls] ??
package/src/types.ts CHANGED
@@ -6,7 +6,28 @@ import type { ImageStyle, TextStyle, ViewStyle } from "react-native";
6
6
 
7
7
  export type RNStyle = ViewStyle | TextStyle | ImageStyle;
8
8
 
9
- export type StyleObject = Record<string, string | number | { width: number; height: number } | undefined>;
9
+ // Transform types for React Native
10
+ export type TransformStyle =
11
+ | { scale?: number }
12
+ | { scaleX?: number }
13
+ | { scaleY?: number }
14
+ | { rotate?: string }
15
+ | { rotateX?: string }
16
+ | { rotateY?: string }
17
+ | { rotateZ?: string }
18
+ | { translateX?: number | string }
19
+ | { translateY?: number | string }
20
+ | { skewX?: string }
21
+ | { skewY?: string }
22
+ | { perspective?: number };
23
+
24
+ export type ShadowOffsetStyle = { width: number; height: number };
25
+
26
+ export type StyleObject = {
27
+ [key: string]: string | number | ShadowOffsetStyle | TransformStyle[] | undefined;
28
+ shadowOffset?: ShadowOffsetStyle;
29
+ transform?: TransformStyle[];
30
+ };
10
31
 
11
32
  export type SpacingValue = number;
12
33
  export type ColorValue = string;