@mgcrea/react-native-tailwind 0.15.3 → 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.
@@ -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);
@@ -0,0 +1,193 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { COLORS, applyOpacity, parseArbitraryColor, parseColorValue } from "./colorUtils";
3
+
4
+ describe("COLORS", () => {
5
+ it("should include basic colors", () => {
6
+ expect(COLORS.white).toBe("#FFFFFF");
7
+ expect(COLORS.black).toBe("#000000");
8
+ expect(COLORS.transparent).toBe("transparent");
9
+ });
10
+
11
+ it("should include flattened Tailwind colors", () => {
12
+ expect(COLORS["red-500"]).toBeDefined();
13
+ expect(COLORS["blue-500"]).toBeDefined();
14
+ expect(COLORS["green-500"]).toBeDefined();
15
+ expect(COLORS["gray-100"]).toBeDefined();
16
+ expect(COLORS["gray-900"]).toBeDefined();
17
+ });
18
+ });
19
+
20
+ describe("applyOpacity", () => {
21
+ it("should apply opacity to 6-digit hex colors", () => {
22
+ expect(applyOpacity("#ff0000", 50)).toBe("#FF000080");
23
+ expect(applyOpacity("#00ff00", 100)).toBe("#00FF00FF");
24
+ expect(applyOpacity("#0000ff", 0)).toBe("#0000FF00");
25
+ });
26
+
27
+ it("should apply opacity to 3-digit hex colors", () => {
28
+ expect(applyOpacity("#f00", 50)).toBe("#FF000080");
29
+ expect(applyOpacity("#0f0", 25)).toBe("#00FF0040");
30
+ expect(applyOpacity("#00f", 75)).toBe("#0000FFBF");
31
+ });
32
+
33
+ it("should handle various opacity values", () => {
34
+ expect(applyOpacity("#000000", 0)).toBe("#00000000");
35
+ expect(applyOpacity("#000000", 25)).toBe("#00000040");
36
+ expect(applyOpacity("#000000", 50)).toBe("#00000080");
37
+ expect(applyOpacity("#000000", 75)).toBe("#000000BF");
38
+ expect(applyOpacity("#000000", 80)).toBe("#000000CC");
39
+ expect(applyOpacity("#000000", 100)).toBe("#000000FF");
40
+ });
41
+
42
+ it("should return transparent unchanged", () => {
43
+ expect(applyOpacity("transparent", 50)).toBe("transparent");
44
+ expect(applyOpacity("transparent", 0)).toBe("transparent");
45
+ expect(applyOpacity("transparent", 100)).toBe("transparent");
46
+ });
47
+
48
+ it("should uppercase the output", () => {
49
+ expect(applyOpacity("#aabbcc", 50)).toBe("#AABBCC80");
50
+ expect(applyOpacity("#AbCdEf", 50)).toBe("#ABCDEF80");
51
+ });
52
+ });
53
+
54
+ describe("parseArbitraryColor", () => {
55
+ it("should parse 6-digit hex colors", () => {
56
+ expect(parseArbitraryColor("[#ff0000]")).toBe("#ff0000");
57
+ expect(parseArbitraryColor("[#00ff00]")).toBe("#00ff00");
58
+ expect(parseArbitraryColor("[#0000ff]")).toBe("#0000ff");
59
+ expect(parseArbitraryColor("[#AABBCC]")).toBe("#AABBCC");
60
+ });
61
+
62
+ it("should parse and expand 3-digit hex colors", () => {
63
+ expect(parseArbitraryColor("[#f00]")).toBe("#ff0000");
64
+ expect(parseArbitraryColor("[#0f0]")).toBe("#00ff00");
65
+ expect(parseArbitraryColor("[#00f]")).toBe("#0000ff");
66
+ expect(parseArbitraryColor("[#ABC]")).toBe("#AABBCC");
67
+ });
68
+
69
+ it("should parse 8-digit hex colors (with alpha)", () => {
70
+ expect(parseArbitraryColor("[#ff000080]")).toBe("#ff000080");
71
+ expect(parseArbitraryColor("[#00ff00cc]")).toBe("#00ff00cc");
72
+ expect(parseArbitraryColor("[#AABBCCDD]")).toBe("#AABBCCDD");
73
+ });
74
+
75
+ it("should preserve input case", () => {
76
+ expect(parseArbitraryColor("[#aabbcc]")).toBe("#aabbcc");
77
+ expect(parseArbitraryColor("[#AABBCC]")).toBe("#AABBCC");
78
+ expect(parseArbitraryColor("[#AaBbCc]")).toBe("#AaBbCc");
79
+ });
80
+
81
+ it("should return null for invalid formats", () => {
82
+ expect(parseArbitraryColor("[#gg0000]")).toBeNull();
83
+ expect(parseArbitraryColor("[#ff00]")).toBeNull();
84
+ expect(parseArbitraryColor("[#ff00000]")).toBeNull();
85
+ expect(parseArbitraryColor("[ff0000]")).toBeNull();
86
+ expect(parseArbitraryColor("#ff0000")).toBeNull();
87
+ expect(parseArbitraryColor("[rgb(255,0,0)]")).toBeNull();
88
+ expect(parseArbitraryColor("")).toBeNull();
89
+ expect(parseArbitraryColor("[]")).toBeNull();
90
+ });
91
+ });
92
+
93
+ describe("parseColorValue", () => {
94
+ describe("preset colors", () => {
95
+ it("should parse preset color names", () => {
96
+ expect(parseColorValue("red-500")).toBe(COLORS["red-500"]);
97
+ expect(parseColorValue("blue-800")).toBe(COLORS["blue-800"]);
98
+ expect(parseColorValue("green-600")).toBe(COLORS["green-600"]);
99
+ });
100
+
101
+ it("should parse basic colors", () => {
102
+ expect(parseColorValue("black")).toBe("#000000");
103
+ expect(parseColorValue("white")).toBe("#FFFFFF");
104
+ expect(parseColorValue("transparent")).toBe("transparent");
105
+ });
106
+
107
+ it("should return null for invalid color names", () => {
108
+ expect(parseColorValue("notacolor")).toBeNull();
109
+ expect(parseColorValue("red-999")).toBeNull();
110
+ expect(parseColorValue("foobar-500")).toBeNull();
111
+ });
112
+ });
113
+
114
+ describe("arbitrary colors", () => {
115
+ it("should parse arbitrary hex colors", () => {
116
+ expect(parseColorValue("[#ff0000]")).toBe("#ff0000");
117
+ expect(parseColorValue("[#00ff00]")).toBe("#00ff00");
118
+ expect(parseColorValue("[#0000ff]")).toBe("#0000ff");
119
+ });
120
+
121
+ it("should parse 3-digit arbitrary hex colors", () => {
122
+ expect(parseColorValue("[#f00]")).toBe("#ff0000");
123
+ expect(parseColorValue("[#0f0]")).toBe("#00ff00");
124
+ expect(parseColorValue("[#00f]")).toBe("#0000ff");
125
+ });
126
+
127
+ it("should parse 8-digit arbitrary hex colors", () => {
128
+ expect(parseColorValue("[#ff000080]")).toBe("#ff000080");
129
+ expect(parseColorValue("[#00ff00cc]")).toBe("#00ff00cc");
130
+ });
131
+ });
132
+
133
+ describe("opacity modifier", () => {
134
+ it("should apply opacity to preset colors", () => {
135
+ expect(parseColorValue("red-500/50")).toBe(applyOpacity(COLORS["red-500"], 50));
136
+ expect(parseColorValue("blue-800/80")).toBe(applyOpacity(COLORS["blue-800"], 80));
137
+ expect(parseColorValue("black/25")).toBe(applyOpacity("#000000", 25));
138
+ });
139
+
140
+ it("should apply opacity to arbitrary colors", () => {
141
+ expect(parseColorValue("[#ff0000]/50")).toBe("#FF000080");
142
+ expect(parseColorValue("[#00ff00]/25")).toBe("#00FF0040");
143
+ expect(parseColorValue("[#0000ff]/80")).toBe("#0000FFCC");
144
+ });
145
+
146
+ it("should handle edge opacity values", () => {
147
+ expect(parseColorValue("red-500/0")).toBe(applyOpacity(COLORS["red-500"], 0));
148
+ expect(parseColorValue("red-500/100")).toBe(applyOpacity(COLORS["red-500"], 100));
149
+ });
150
+
151
+ it("should return null for invalid opacity values", () => {
152
+ expect(parseColorValue("red-500/101")).toBeNull();
153
+ expect(parseColorValue("red-500/-1")).toBeNull();
154
+ expect(parseColorValue("red-500/abc")).toBeNull();
155
+ });
156
+
157
+ it("should keep transparent unchanged with opacity", () => {
158
+ expect(parseColorValue("transparent/50")).toBe("transparent");
159
+ expect(parseColorValue("transparent/0")).toBe("transparent");
160
+ });
161
+ });
162
+
163
+ describe("custom colors", () => {
164
+ const customColors = {
165
+ brand: "#FF5733",
166
+ "brand-primary": "#3498DB",
167
+ "brand-secondary": "#2ECC71",
168
+ };
169
+
170
+ it("should parse custom colors", () => {
171
+ expect(parseColorValue("brand", customColors)).toBe("#FF5733");
172
+ expect(parseColorValue("brand-primary", customColors)).toBe("#3498DB");
173
+ expect(parseColorValue("brand-secondary", customColors)).toBe("#2ECC71");
174
+ });
175
+
176
+ it("should apply opacity to custom colors", () => {
177
+ expect(parseColorValue("brand/50", customColors)).toBe("#FF573380");
178
+ expect(parseColorValue("brand-primary/80", customColors)).toBe("#3498DBCC");
179
+ });
180
+
181
+ it("should still support preset colors with custom colors", () => {
182
+ expect(parseColorValue("red-500", customColors)).toBe(COLORS["red-500"]);
183
+ expect(parseColorValue("blue-800/50", customColors)).toBe(applyOpacity(COLORS["blue-800"], 50));
184
+ });
185
+
186
+ it("should allow custom colors to override presets", () => {
187
+ const overrideColors = {
188
+ "red-500": "#CUSTOM1",
189
+ };
190
+ expect(parseColorValue("red-500", overrideColors)).toBe("#CUSTOM1");
191
+ });
192
+ });
193
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Shared color utilities for parsing and manipulating colors
3
+ */
4
+
5
+ import { TAILWIND_COLORS } from "../config/tailwind";
6
+ import { flattenColors } from "./flattenColors";
7
+
8
+ /**
9
+ * Tailwind color palette (flattened from config) with basic colors
10
+ */
11
+ export const COLORS: Record<string, string> = {
12
+ ...flattenColors(TAILWIND_COLORS),
13
+ white: "#FFFFFF",
14
+ black: "#000000",
15
+ transparent: "transparent",
16
+ };
17
+
18
+ /**
19
+ * Apply opacity to hex color by appending alpha channel
20
+ * @param hex - Hex color string (e.g., "#ff0000", "#f00", or "transparent")
21
+ * @param opacity - Opacity value 0-100 (e.g., 50 for 50%)
22
+ * @returns 8-digit hex with alpha (e.g., "#FF000080") or transparent
23
+ */
24
+ export function applyOpacity(hex: string, opacity: number): string {
25
+ if (hex === "transparent") {
26
+ return "transparent";
27
+ }
28
+
29
+ const cleanHex = hex.replace(/^#/, "");
30
+ const fullHex =
31
+ cleanHex.length === 3
32
+ ? cleanHex
33
+ .split("")
34
+ .map((char) => char + char)
35
+ .join("")
36
+ : cleanHex;
37
+
38
+ const alpha = Math.round((opacity / 100) * 255);
39
+ const alphaHex = alpha.toString(16).padStart(2, "0").toUpperCase();
40
+
41
+ return `#${fullHex.toUpperCase()}${alphaHex}`;
42
+ }
43
+
44
+ /**
45
+ * Parse arbitrary color value: [#ff0000], [#f00], [#FF0000AA]
46
+ * Supports 3-digit, 6-digit, and 8-digit (with alpha) hex colors
47
+ * @param value - Arbitrary value string like "[#ff0000]"
48
+ * @returns Hex string if valid, null otherwise (preserves input case)
49
+ */
50
+ export function parseArbitraryColor(value: string): string | null {
51
+ const hexMatch = value.match(/^\[#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\]$/);
52
+ if (hexMatch) {
53
+ const hex = hexMatch[1];
54
+ if (hex.length === 3) {
55
+ // Expand 3-digit hex to 6-digit: #abc -> #aabbcc (preserve case)
56
+ const expanded = hex
57
+ .split("")
58
+ .map((char) => char + char)
59
+ .join("");
60
+ return `#${expanded}`;
61
+ }
62
+ return `#${hex}`;
63
+ }
64
+ return null;
65
+ }
66
+
67
+ /**
68
+ * Parse a color value with optional opacity modifier
69
+ * Handles preset colors, custom colors, arbitrary hex values, and opacity modifiers
70
+ *
71
+ * @param colorKey - Color key like "red-500", "red-500/50", "[#ff0000]", "[#ff0000]/80"
72
+ * @param customColors - Optional custom colors from tailwind.config
73
+ * @returns Hex color string or null if invalid
74
+ */
75
+ export function parseColorValue(colorKey: string, customColors?: Record<string, string>): string | null {
76
+ const getColor = (key: string): string | undefined => {
77
+ return customColors?.[key] ?? COLORS[key];
78
+ };
79
+
80
+ // Check for opacity modifier: red-500/50, [#ff0000]/80
81
+ const opacityMatch = colorKey.match(/^(.+)\/(\d+)$/);
82
+ if (opacityMatch) {
83
+ const baseColorKey = opacityMatch[1];
84
+ const opacity = Number.parseInt(opacityMatch[2], 10);
85
+
86
+ // Validate opacity range (0-100)
87
+ if (opacity < 0 || opacity > 100) {
88
+ return null;
89
+ }
90
+
91
+ // Try arbitrary color first: [#ff0000]/50
92
+ const arbitraryColor = parseArbitraryColor(baseColorKey);
93
+ if (arbitraryColor !== null) {
94
+ return applyOpacity(arbitraryColor, opacity);
95
+ }
96
+
97
+ // Try preset/custom colors: red-500/50
98
+ const color = getColor(baseColorKey);
99
+ if (color) {
100
+ return applyOpacity(color, opacity);
101
+ }
102
+
103
+ return null;
104
+ }
105
+
106
+ // No opacity modifier
107
+ // Try arbitrary value first: [#ff0000]
108
+ const arbitraryColor = parseArbitraryColor(colorKey);
109
+ if (arbitraryColor !== null) {
110
+ return arbitraryColor;
111
+ }
112
+
113
+ // Try preset/custom colors: red-500
114
+ return getColor(colorKey) ?? null;
115
+ }