@mgcrea/react-native-tailwind 0.15.4 → 0.15.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mgcrea/react-native-tailwind",
3
- "version": "0.15.4",
3
+ "version": "0.15.5",
4
4
  "description": "Compile-time Tailwind CSS for React Native with zero runtime overhead",
5
5
  "author": "Olivier Louvignes <olivier@mgcrea.io> (https://github.com/mgcrea)",
6
6
  "homepage": "https://github.com/mgcrea/react-native-tailwind#readme",
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ export {
25
25
  parseBorder,
26
26
  parseColor,
27
27
  parseLayout,
28
+ parseOutline,
28
29
  parsePlaceholderClass,
29
30
  parsePlaceholderClasses,
30
31
  parseShadow,
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { COLORS, parseColor } from "./colors";
3
2
  import { applyOpacity } from "../utils/colorUtils";
3
+ import { COLORS, parseColor } from "./colors";
4
4
 
5
5
  describe("COLORS", () => {
6
6
  it("should export complete color palette", () => {
@@ -116,6 +116,25 @@ export function parseColor(cls: string, customColors?: Record<string, string>):
116
116
  }
117
117
  }
118
118
 
119
+ // Outline color: outline-blue-500, outline-blue-500/50, outline-[#ff0000]/80
120
+ if (cls.startsWith("outline-") && !cls.match(/^outline-[0-9]/) && !cls.startsWith("outline-offset-")) {
121
+ const colorKey = cls.substring(8); // "outline-".length = 8
122
+
123
+ // Skip outline-style values
124
+ if (["solid", "dashed", "dotted", "none"].includes(colorKey)) {
125
+ return null;
126
+ }
127
+
128
+ // Skip arbitrary values that don't look like colors (e.g., outline-[3px] is width)
129
+ if (colorKey.startsWith("[") && !colorKey.startsWith("[#")) {
130
+ return null;
131
+ }
132
+ const color = parseColorWithOpacity(colorKey);
133
+ if (color) {
134
+ return { outlineColor: color };
135
+ }
136
+ }
137
+
119
138
  // Directional border colors: border-t-red-500, border-l-blue-500/50, border-r-[#ff0000]
120
139
  const dirBorderMatch = cls.match(/^border-([trblxy])-(.+)$/);
121
140
  if (dirBorderMatch) {
@@ -9,6 +9,7 @@ import { parseAspectRatio } from "./aspectRatio";
9
9
  import { parseBorder } from "./borders";
10
10
  import { parseColor } from "./colors";
11
11
  import { parseLayout } from "./layout";
12
+ import { parseOutline } from "./outline";
12
13
  import { parseShadow } from "./shadows";
13
14
  import { parseSizing } from "./sizing";
14
15
  import { parseSpacing } from "./spacing";
@@ -56,6 +57,7 @@ export function parseClass(cls: string, customTheme?: CustomTheme): StyleObject
56
57
  const parsers: Array<(cls: string) => StyleObject | null> = [
57
58
  (cls: string) => parseSpacing(cls, customTheme?.spacing),
58
59
  (cls: string) => parseBorder(cls, customTheme?.colors),
60
+ parseOutline,
59
61
  (cls: string) => parseColor(cls, customTheme?.colors),
60
62
  (cls: string) => parseLayout(cls, customTheme?.spacing),
61
63
  (cls: string) => parseTypography(cls, customTheme?.fontFamily, customTheme?.fontSize),
@@ -86,6 +88,7 @@ export { parseAspectRatio } from "./aspectRatio";
86
88
  export { parseBorder } from "./borders";
87
89
  export { parseColor } from "./colors";
88
90
  export { parseLayout } from "./layout";
91
+ export { parseOutline } from "./outline";
89
92
  export { parsePlaceholderClass, parsePlaceholderClasses } from "./placeholder";
90
93
  export { parseShadow } from "./shadows";
91
94
  export { parseSizing } from "./sizing";
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parseOutline } from "./outline";
3
+
4
+ describe("parseOutline", () => {
5
+ it("should parse outline shorthand", () => {
6
+ expect(parseOutline("outline")).toEqual({
7
+ outlineWidth: 1,
8
+ outlineStyle: "solid",
9
+ });
10
+ });
11
+
12
+ it("should parse outline-none", () => {
13
+ expect(parseOutline("outline-none")).toEqual({ outlineWidth: 0 });
14
+ });
15
+
16
+ it("should parse outline width with preset values", () => {
17
+ expect(parseOutline("outline-0")).toEqual({ outlineWidth: 0 });
18
+ expect(parseOutline("outline-2")).toEqual({ outlineWidth: 2 });
19
+ expect(parseOutline("outline-4")).toEqual({ outlineWidth: 4 });
20
+ expect(parseOutline("outline-8")).toEqual({ outlineWidth: 8 });
21
+ });
22
+
23
+ it("should parse outline width with arbitrary values", () => {
24
+ expect(parseOutline("outline-[5px]")).toEqual({ outlineWidth: 5 });
25
+ expect(parseOutline("outline-[10]")).toEqual({ outlineWidth: 10 });
26
+ });
27
+
28
+ it("should parse outline style", () => {
29
+ expect(parseOutline("outline-solid")).toEqual({ outlineStyle: "solid" });
30
+ expect(parseOutline("outline-dashed")).toEqual({ outlineStyle: "dashed" });
31
+ expect(parseOutline("outline-dotted")).toEqual({ outlineStyle: "dotted" });
32
+ });
33
+
34
+ it("should parse outline offset with preset values", () => {
35
+ expect(parseOutline("outline-offset-0")).toEqual({ outlineOffset: 0 });
36
+ expect(parseOutline("outline-offset-2")).toEqual({ outlineOffset: 2 });
37
+ expect(parseOutline("outline-offset-4")).toEqual({ outlineOffset: 4 });
38
+ expect(parseOutline("outline-offset-8")).toEqual({ outlineOffset: 8 });
39
+ });
40
+
41
+ it("should parse outline offset with arbitrary values", () => {
42
+ expect(parseOutline("outline-offset-[3px]")).toEqual({ outlineOffset: 3 });
43
+ expect(parseOutline("outline-offset-[5]")).toEqual({ outlineOffset: 5 });
44
+ });
45
+
46
+ it("should return null for invalid outline values", () => {
47
+ expect(parseOutline("outline-invalid")).toBeNull();
48
+ expect(parseOutline("outline-3")).toBeNull(); // Not in scale
49
+ expect(parseOutline("outline-offset-3")).toBeNull(); // Not in scale
50
+ expect(parseOutline("outline-[5rem]")).toBeNull(); // Unsupported unit
51
+ });
52
+
53
+ it("should return null for outline colors (handled by parseColor)", () => {
54
+ expect(parseOutline("outline-red-500")).toBeNull();
55
+ expect(parseOutline("outline-[#ff0000]")).toBeNull();
56
+ });
57
+ });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Outline utilities (outline width, style, offset)
3
+ */
4
+
5
+ import type { StyleObject } from "../types";
6
+ import { BORDER_WIDTH_SCALE } from "./borders";
7
+
8
+ /**
9
+ * Parse arbitrary outline width/offset value: [8px], [4]
10
+ * Returns number for px values, null for unsupported formats
11
+ */
12
+ function parseArbitraryOutlineValue(value: string): number | null {
13
+ // Match: [8px] or [8] (pixels only)
14
+ const pxMatch = value.match(/^\[(\d+)(?:px)?\]$/);
15
+ if (pxMatch) {
16
+ return parseInt(pxMatch[1], 10);
17
+ }
18
+
19
+ // Warn about unsupported formats
20
+ if (value.startsWith("[") && value.endsWith("]")) {
21
+ /* v8 ignore next 5 */
22
+ if (process.env.NODE_ENV !== "production") {
23
+ console.warn(
24
+ `[react-native-tailwind] Unsupported arbitrary outline value: ${value}. Only px values are supported (e.g., [8px] or [8]).`,
25
+ );
26
+ }
27
+ return null;
28
+ }
29
+
30
+ return null;
31
+ }
32
+
33
+ /**
34
+ * Parse outline classes
35
+ * @param cls - The class name to parse
36
+ */
37
+ export function parseOutline(cls: string): StyleObject | null {
38
+ // Shorthand: outline (width: 1, style: solid)
39
+ if (cls === "outline") {
40
+ return { outlineWidth: 1, outlineStyle: "solid" };
41
+ }
42
+
43
+ // Outline none
44
+ if (cls === "outline-none") {
45
+ return { outlineWidth: 0 };
46
+ }
47
+
48
+ // Outline style
49
+ if (cls === "outline-solid") return { outlineStyle: "solid" };
50
+ if (cls === "outline-dotted") return { outlineStyle: "dotted" };
51
+ if (cls === "outline-dashed") return { outlineStyle: "dashed" };
52
+
53
+ // Outline offset: outline-offset-2, outline-offset-[3px]
54
+ if (cls.startsWith("outline-offset-")) {
55
+ const valueStr = cls.substring(15); // "outline-offset-".length = 15
56
+
57
+ // Try arbitrary value first
58
+ if (valueStr.startsWith("[")) {
59
+ const arbitraryValue = parseArbitraryOutlineValue(valueStr);
60
+ if (arbitraryValue !== null) {
61
+ return { outlineOffset: arbitraryValue };
62
+ }
63
+ return null;
64
+ }
65
+
66
+ // Try preset scale (reuse border width scale for consistency with default Tailwind)
67
+ const scaleValue = BORDER_WIDTH_SCALE[valueStr];
68
+ if (scaleValue !== undefined) {
69
+ return { outlineOffset: scaleValue };
70
+ }
71
+
72
+ return null;
73
+ }
74
+
75
+ // Outline width: outline-0, outline-2, outline-[5px]
76
+ // Must handle potential collision with outline-red-500 (colors)
77
+ // Logic: if it matches width pattern, return width. If it looks like color, return null (let parseColor handle it)
78
+
79
+ const widthMatch = cls.match(/^outline-(\d+)$/);
80
+ if (widthMatch) {
81
+ const value = BORDER_WIDTH_SCALE[widthMatch[1]];
82
+ if (value !== undefined) {
83
+ return { outlineWidth: value };
84
+ }
85
+ }
86
+
87
+ const arbMatch = cls.match(/^outline-(\[.+\])$/);
88
+ if (arbMatch) {
89
+ // Check if it's a color first? No, colors usually look like [#...] or [rgb(...)]
90
+ // parseArbitraryOutlineValue only accepts [123] or [123px]
91
+ // If it fails, it might be a color, so we return null
92
+ const arbitraryValue = parseArbitraryOutlineValue(arbMatch[1]);
93
+ if (arbitraryValue !== null) {
94
+ return { outlineWidth: arbitraryValue };
95
+ }
96
+ return null;
97
+ }
98
+
99
+ // If it's outline-{color}, return null so parseColor (called later in index.ts) handles it
100
+ return null;
101
+ }
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { SHADOW_COLORS, SHADOW_SCALE, parseShadow } from "./shadows";
3
2
  import { applyOpacity } from "../utils/colorUtils";
3
+ import { SHADOW_COLORS, SHADOW_SCALE, parseShadow } from "./shadows";
4
4
 
5
5
  describe("SHADOW_SCALE", () => {
6
6
  it("should export complete shadow scale", () => {
@@ -97,4 +97,4 @@ export function parseShadow(cls: string, customColors?: Record<string, string>):
97
97
  }
98
98
 
99
99
  // Export shadow scale and colors for testing/advanced usage
100
- export { SHADOW_SCALE, COLORS as SHADOW_COLORS };
100
+ export { COLORS as SHADOW_COLORS, SHADOW_SCALE };
@@ -310,3 +310,85 @@ describe("parseSizing - custom spacing", () => {
310
310
  expect(parseSizing("h-8")).toEqual({ height: 32 }); // Default behavior
311
311
  });
312
312
  });
313
+
314
+ describe("parseSizing - size (width + height)", () => {
315
+ it("should parse size with numeric values", () => {
316
+ expect(parseSizing("size-0")).toEqual({ width: 0, height: 0 });
317
+ expect(parseSizing("size-4")).toEqual({ width: 16, height: 16 });
318
+ expect(parseSizing("size-8")).toEqual({ width: 32, height: 32 });
319
+ expect(parseSizing("size-96")).toEqual({ width: 384, height: 384 });
320
+ });
321
+
322
+ it("should parse size with fractional values", () => {
323
+ expect(parseSizing("size-0.5")).toEqual({ width: 2, height: 2 });
324
+ expect(parseSizing("size-1.5")).toEqual({ width: 6, height: 6 });
325
+ expect(parseSizing("size-2.5")).toEqual({ width: 10, height: 10 });
326
+ });
327
+
328
+ it("should parse size with percentage values", () => {
329
+ expect(parseSizing("size-full")).toEqual({ width: "100%", height: "100%" });
330
+ expect(parseSizing("size-1/2")).toEqual({ width: "50%", height: "50%" });
331
+ expect(parseSizing("size-1/3")).toEqual({ width: "33.333333%", height: "33.333333%" });
332
+ expect(parseSizing("size-2/3")).toEqual({ width: "66.666667%", height: "66.666667%" });
333
+ expect(parseSizing("size-1/4")).toEqual({ width: "25%", height: "25%" });
334
+ expect(parseSizing("size-3/4")).toEqual({ width: "75%", height: "75%" });
335
+ });
336
+
337
+ it("should parse size with special values", () => {
338
+ expect(parseSizing("size-auto")).toEqual({ width: "auto", height: "auto" });
339
+ });
340
+
341
+ it("should parse size with arbitrary pixel values", () => {
342
+ expect(parseSizing("size-[123px]")).toEqual({ width: 123, height: 123 });
343
+ expect(parseSizing("size-[200]")).toEqual({ width: 200, height: 200 });
344
+ expect(parseSizing("size-[50px]")).toEqual({ width: 50, height: 50 });
345
+ });
346
+
347
+ it("should parse size with arbitrary percentage values", () => {
348
+ expect(parseSizing("size-[50%]")).toEqual({ width: "50%", height: "50%" });
349
+ expect(parseSizing("size-[33.333%]")).toEqual({ width: "33.333%", height: "33.333%" });
350
+ expect(parseSizing("size-[85%]")).toEqual({ width: "85%", height: "85%" });
351
+ });
352
+
353
+ it("should support custom spacing values for size", () => {
354
+ const customSpacing = { sm: 8, md: 16, lg: 32, xl: 64 };
355
+ expect(parseSizing("size-sm", customSpacing)).toEqual({ width: 8, height: 8 });
356
+ expect(parseSizing("size-md", customSpacing)).toEqual({ width: 16, height: 16 });
357
+ expect(parseSizing("size-lg", customSpacing)).toEqual({ width: 32, height: 32 });
358
+ expect(parseSizing("size-xl", customSpacing)).toEqual({ width: 64, height: 64 });
359
+ });
360
+
361
+ it("should allow custom spacing to override preset values", () => {
362
+ const customSpacing = { "4": 20 }; // Override default (16)
363
+ expect(parseSizing("size-4", customSpacing)).toEqual({ width: 20, height: 20 });
364
+ });
365
+
366
+ it("should prefer arbitrary values over custom spacing", () => {
367
+ expect(parseSizing("size-[24px]", { "4": 20 })).toEqual({ width: 24, height: 24 });
368
+ expect(parseSizing("size-[50]", { sm: 8 })).toEqual({ width: 50, height: 50 });
369
+ });
370
+
371
+ it("should handle all fractional percentage variants", () => {
372
+ expect(parseSizing("size-1/5")).toEqual({ width: "20%", height: "20%" });
373
+ expect(parseSizing("size-2/5")).toEqual({ width: "40%", height: "40%" });
374
+ expect(parseSizing("size-3/5")).toEqual({ width: "60%", height: "60%" });
375
+ expect(parseSizing("size-4/5")).toEqual({ width: "80%", height: "80%" });
376
+ expect(parseSizing("size-1/6")).toEqual({ width: "16.666667%", height: "16.666667%" });
377
+ expect(parseSizing("size-5/6")).toEqual({ width: "83.333333%", height: "83.333333%" });
378
+ });
379
+
380
+ it("should handle edge case size values", () => {
381
+ expect(parseSizing("size-0")).toEqual({ width: 0, height: 0 });
382
+ });
383
+
384
+ it("should return null for invalid size values", () => {
385
+ expect(parseSizing("size-invalid")).toBeNull();
386
+ expect(parseSizing("size-999")).toBeNull();
387
+ });
388
+
389
+ it("should return null for arbitrary values with unsupported units", () => {
390
+ expect(parseSizing("size-[16rem]")).toBeNull();
391
+ expect(parseSizing("size-[2em]")).toBeNull();
392
+ expect(parseSizing("size-[50vh]")).toBeNull();
393
+ });
394
+ });
@@ -102,6 +102,34 @@ export function parseSizing(cls: string, customSpacing?: Record<string, number>)
102
102
  // Merge custom spacing with defaults (custom takes precedence)
103
103
  const sizeMap = customSpacing ? { ...SIZE_SCALE, ...customSpacing } : SIZE_SCALE;
104
104
 
105
+ // Size (both width AND height)
106
+ if (cls.startsWith("size-")) {
107
+ const sizeKey = cls.substring(5); // "size-".length = 5
108
+
109
+ // Arbitrary values: size-[123px], size-[50%] (highest priority)
110
+ const arbitrarySize = parseArbitrarySize(sizeKey);
111
+ if (arbitrarySize !== null) {
112
+ return { width: arbitrarySize, height: arbitrarySize };
113
+ }
114
+
115
+ // Percentage sizes: size-full, size-1/2, etc.
116
+ const percentage = SIZE_PERCENTAGES[sizeKey];
117
+ if (percentage) {
118
+ return { width: percentage, height: percentage };
119
+ }
120
+
121
+ // Numeric sizes: size-4, size-8, etc. (includes custom spacing)
122
+ const numericSize = sizeMap[sizeKey];
123
+ if (numericSize !== undefined) {
124
+ return { width: numericSize, height: numericSize };
125
+ }
126
+
127
+ // Special values
128
+ if (sizeKey === "auto") {
129
+ return { width: "auto", height: "auto" };
130
+ }
131
+ }
132
+
105
133
  // Width
106
134
  if (cls.startsWith("w-")) {
107
135
  const sizeKey = cls.substring(2);