@mgcrea/react-native-tailwind 0.5.2 → 0.6.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.
@@ -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 });
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;