Skip to content

Commit 663dc5e

Browse files
committed
refactor: extract shared color utilities to colorUtils.ts
Consolidate duplicate color parsing logic from colors.ts and shadows.ts into a shared utility module. This eliminates ~100 lines of duplicated code while maintaining backward compatibility. Changes: - Create src/utils/colorUtils.ts with shared utilities: - COLORS constant (flattened Tailwind colors + basic colors) - applyOpacity() - Apply opacity to hex colors - parseArbitraryColor() - Parse arbitrary hex values [#ff0000] - parseColorValue() - Parse colors with optional opacity modifiers - Refactor shadows.ts to use shared parseColorValue() - Refactor colors.ts to use shared utilities - Update test files to import applyOpacity from shared utility - Preserve input case for arbitrary colors, uppercase when applying opacity
1 parent d1b03d4 commit 663dc5e

File tree

5 files changed

+144
-222
lines changed

5 files changed

+144
-222
lines changed

src/parser/colors.test.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,6 @@
11
import { describe, expect, it } from "vitest";
22
import { COLORS, parseColor } from "./colors";
3-
4-
// Helper to apply opacity to hex color for testing
5-
function applyOpacity(hex: string, opacity: number): string {
6-
if (hex === "transparent") return "transparent";
7-
const cleanHex = hex.replace(/^#/, "");
8-
const fullHex =
9-
cleanHex.length === 3
10-
? cleanHex
11-
.split("")
12-
.map((char) => char + char)
13-
.join("")
14-
: cleanHex;
15-
const alpha = Math.round((opacity / 100) * 255);
16-
const alphaHex = alpha.toString(16).padStart(2, "0").toUpperCase();
17-
return `#${fullHex.toUpperCase()}${alphaHex}`;
18-
}
3+
import { applyOpacity } from "../utils/colorUtils";
194

205
describe("COLORS", () => {
216
it("should export complete color palette", () => {

src/parser/colors.ts

Lines changed: 15 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -2,85 +2,11 @@
22
* Color utilities (background, text, border colors)
33
*/
44

5-
import { TAILWIND_COLORS } from "../config/tailwind";
65
import type { StyleObject } from "../types";
7-
import { flattenColors } from "../utils/flattenColors";
6+
import { COLORS, applyOpacity, parseArbitraryColor } from "../utils/colorUtils";
87

9-
// Tailwind color palette (flattened from config)
10-
export const COLORS: Record<string, string> = {
11-
...flattenColors(TAILWIND_COLORS),
12-
// Add basic 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 rgba for special colors
23-
*/
24-
function applyOpacity(hex: string, opacity: number): string {
25-
// Handle transparent specially
26-
if (hex === "transparent") {
27-
return "transparent";
28-
}
29-
30-
// Remove # if present
31-
const cleanHex = hex.replace(/^#/, "");
32-
33-
// Expand 3-digit hex to 6-digit: #abc -> #aabbcc
34-
const fullHex =
35-
cleanHex.length === 3
36-
? cleanHex
37-
.split("")
38-
.map((char) => char + char)
39-
.join("")
40-
: cleanHex;
41-
42-
// Convert opacity percentage (0-100) to hex (00-FF)
43-
const alpha = Math.round((opacity / 100) * 255);
44-
const alphaHex = alpha.toString(16).padStart(2, "0").toUpperCase();
45-
46-
// Return 8-digit hex: #RRGGBBAA
47-
return `#${fullHex.toUpperCase()}${alphaHex}`;
48-
}
49-
50-
/**
51-
* Parse arbitrary color value: [#ff0000], [#f00], [#FF0000AA]
52-
* Supports 3-digit, 6-digit, and 8-digit (with alpha) hex colors
53-
* Returns hex string if valid, null otherwise
54-
*/
55-
function parseArbitraryColor(value: string): string | null {
56-
// Match: [#rgb], [#rrggbb], or [#rrggbbaa]
57-
const hexMatch = value.match(/^\[#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\]$/);
58-
if (hexMatch) {
59-
const hex = hexMatch[1];
60-
// Expand 3-digit hex to 6-digit: #abc -> #aabbcc
61-
if (hex.length === 3) {
62-
const expanded = hex
63-
.split("")
64-
.map((char) => char + char)
65-
.join("");
66-
return `#${expanded}`;
67-
}
68-
return `#${hex}`;
69-
}
70-
71-
// Warn about unsupported formats
72-
if (value.startsWith("[") && value.endsWith("]")) {
73-
/* v8 ignore next 5 */
74-
if (process.env.NODE_ENV !== "production") {
75-
console.warn(
76-
`[react-native-tailwind] Unsupported arbitrary color value: ${value}. Only hex colors are supported (e.g., [#ff0000], [#f00], or [#ff0000aa]).`,
77-
);
78-
}
79-
return null;
80-
}
81-
82-
return null;
83-
}
8+
// Re-export COLORS for backward compatibility and tests
9+
export { COLORS };
8410

8511
/**
8612
* Parse color classes (background, text, border)
@@ -93,6 +19,7 @@ export function parseColor(cls: string, customColors?: Record<string, string>):
9319
};
9420

9521
// Helper to parse color with optional opacity modifier
22+
// Uses internal implementation to preserve warnings for invalid arbitrary colors
9623
const parseColorWithOpacity = (colorKey: string): string | null => {
9724
// Check for opacity modifier: blue-500/50
9825
const opacityMatch = colorKey.match(/^(.+)\/(\d+)$/);
@@ -133,6 +60,17 @@ export function parseColor(cls: string, customColors?: Record<string, string>):
13360
return arbitraryColor;
13461
}
13562

63+
// Check for unsupported arbitrary format and warn
64+
if (colorKey.startsWith("[") && colorKey.endsWith("]")) {
65+
/* v8 ignore next 5 */
66+
if (process.env.NODE_ENV !== "production") {
67+
console.warn(
68+
`[react-native-tailwind] Unsupported arbitrary color value: ${colorKey}. Only hex colors are supported (e.g., [#ff0000], [#f00], or [#ff0000aa]).`,
69+
);
70+
}
71+
return null;
72+
}
73+
13674
// Try preset/custom colors
13775
return getColor(colorKey) ?? null;
13876
};

src/parser/shadows.test.ts

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,6 @@
11
import { describe, expect, it } from "vitest";
22
import { SHADOW_COLORS, SHADOW_SCALE, parseShadow } from "./shadows";
3-
4-
// Helper to apply opacity to hex color for testing (same as colors.test.ts)
5-
function applyOpacity(hex: string, opacity: number): string {
6-
if (hex === "transparent") return "transparent";
7-
const cleanHex = hex.replace(/^#/, "");
8-
const fullHex =
9-
cleanHex.length === 3
10-
? cleanHex
11-
.split("")
12-
.map((char) => char + char)
13-
.join("")
14-
: cleanHex;
15-
const alpha = Math.round((opacity / 100) * 255);
16-
const alphaHex = alpha.toString(16).padStart(2, "0").toUpperCase();
17-
return `#${fullHex.toUpperCase()}${alphaHex}`;
18-
}
3+
import { applyOpacity } from "../utils/colorUtils";
194

205
describe("SHADOW_SCALE", () => {
216
it("should export complete shadow scale", () => {
@@ -239,26 +224,27 @@ describe("parseShadow - shadow colors", () => {
239224
});
240225

241226
it("should parse shadow color with arbitrary hex values", () => {
242-
expect(parseShadow("shadow-[#ff0000]")).toEqual({ shadowColor: "#FF0000" });
243-
expect(parseShadow("shadow-[#00ff00]")).toEqual({ shadowColor: "#00FF00" });
244-
expect(parseShadow("shadow-[#0000ff]")).toEqual({ shadowColor: "#0000FF" });
227+
expect(parseShadow("shadow-[#ff0000]")).toEqual({ shadowColor: "#ff0000" });
228+
expect(parseShadow("shadow-[#00ff00]")).toEqual({ shadowColor: "#00ff00" });
229+
expect(parseShadow("shadow-[#0000ff]")).toEqual({ shadowColor: "#0000ff" });
245230
});
246231

247232
it("should parse shadow color with 3-digit hex values", () => {
248-
expect(parseShadow("shadow-[#f00]")).toEqual({ shadowColor: "#FF0000" });
249-
expect(parseShadow("shadow-[#0f0]")).toEqual({ shadowColor: "#00FF00" });
250-
expect(parseShadow("shadow-[#00f]")).toEqual({ shadowColor: "#0000FF" });
233+
expect(parseShadow("shadow-[#f00]")).toEqual({ shadowColor: "#ff0000" });
234+
expect(parseShadow("shadow-[#0f0]")).toEqual({ shadowColor: "#00ff00" });
235+
expect(parseShadow("shadow-[#00f]")).toEqual({ shadowColor: "#0000ff" });
251236
});
252237

253238
it("should parse shadow color with arbitrary hex and opacity", () => {
239+
// When opacity is applied, the color is uppercased for consistency
254240
expect(parseShadow("shadow-[#ff0000]/50")).toEqual({ shadowColor: "#FF000080" });
255241
expect(parseShadow("shadow-[#00ff00]/25")).toEqual({ shadowColor: "#00FF0040" });
256242
expect(parseShadow("shadow-[#0000ff]/80")).toEqual({ shadowColor: "#0000FFCC" });
257243
});
258244

259245
it("should parse shadow color with 8-digit hex (with alpha)", () => {
260-
expect(parseShadow("shadow-[#ff000080]")).toEqual({ shadowColor: "#FF000080" });
261-
expect(parseShadow("shadow-[#00ff00cc]")).toEqual({ shadowColor: "#00FF00CC" });
246+
expect(parseShadow("shadow-[#ff000080]")).toEqual({ shadowColor: "#ff000080" });
247+
expect(parseShadow("shadow-[#00ff00cc]")).toEqual({ shadowColor: "#00ff00cc" });
262248
});
263249

264250
it("should handle transparent with opacity modifier", () => {

src/parser/shadows.ts

Lines changed: 3 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,8 @@
33
* iOS uses shadow* properties, Android uses elevation
44
*/
55

6-
import { TAILWIND_COLORS } from "../config/tailwind";
76
import type { StyleObject } from "../types";
8-
import { flattenColors } from "../utils/flattenColors";
9-
10-
// Default colors (same as colors.ts)
11-
const COLORS: Record<string, string> = {
12-
...flattenColors(TAILWIND_COLORS),
13-
white: "#FFFFFF",
14-
black: "#000000",
15-
transparent: "transparent",
16-
};
7+
import { COLORS, parseColorValue } from "../utils/colorUtils";
178

189
/**
1910
* Shadow scale definitions combining iOS and Android properties
@@ -95,8 +86,8 @@ export function parseShadow(cls: string, customColors?: Record<string, string>):
9586
if (cls.startsWith("shadow-")) {
9687
const colorPart = cls.substring(7); // Remove "shadow-"
9788

98-
// Parse the color value
99-
const shadowColor = parseShadowColorValue(colorPart, customColors);
89+
// Parse the color value using shared utility
90+
const shadowColor = parseColorValue(colorPart, customColors);
10091
if (shadowColor) {
10192
return { shadowColor };
10293
}
@@ -105,98 +96,5 @@ export function parseShadow(cls: string, customColors?: Record<string, string>):
10596
return null;
10697
}
10798

108-
/**
109-
* Apply opacity to hex color by appending alpha channel
110-
* @param hex - Hex color string (e.g., "#ff0000", "#f00", or "transparent")
111-
* @param opacity - Opacity value 0-100 (e.g., 50 for 50%)
112-
* @returns 8-digit hex with alpha (e.g., "#FF000080") or transparent
113-
*/
114-
function applyOpacity(hex: string, opacity: number): string {
115-
if (hex === "transparent") {
116-
return "transparent";
117-
}
118-
119-
const cleanHex = hex.replace(/^#/, "");
120-
const fullHex =
121-
cleanHex.length === 3
122-
? cleanHex
123-
.split("")
124-
.map((char) => char + char)
125-
.join("")
126-
: cleanHex;
127-
128-
const alpha = Math.round((opacity / 100) * 255);
129-
const alphaHex = alpha.toString(16).padStart(2, "0").toUpperCase();
130-
131-
return `#${fullHex.toUpperCase()}${alphaHex}`;
132-
}
133-
134-
/**
135-
* Parse arbitrary color value: [#ff0000], [#f00], [#FF0000AA]
136-
*/
137-
function parseArbitraryColor(value: string): string | null {
138-
const hexMatch = value.match(/^\[#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\]$/);
139-
if (hexMatch) {
140-
const hex = hexMatch[1];
141-
if (hex.length === 3) {
142-
const expanded = hex
143-
.split("")
144-
.map((char) => char + char)
145-
.join("");
146-
return `#${expanded.toUpperCase()}`;
147-
}
148-
return `#${hex.toUpperCase()}`;
149-
}
150-
return null;
151-
}
152-
153-
/**
154-
* Parse shadow color value with optional opacity
155-
* @param colorKey - Color key like "red-500", "red-500/50", "[#ff0000]", "[#ff0000]/80"
156-
* @param customColors - Optional custom colors from tailwind.config
157-
* @returns Hex color string or null if invalid
158-
*/
159-
function parseShadowColorValue(colorKey: string, customColors?: Record<string, string>): string | null {
160-
const getColor = (key: string): string | undefined => {
161-
return customColors?.[key] ?? COLORS[key];
162-
};
163-
164-
// Check for opacity modifier: red-500/50, [#ff0000]/80
165-
const opacityMatch = colorKey.match(/^(.+)\/(\d+)$/);
166-
if (opacityMatch) {
167-
const baseColorKey = opacityMatch[1];
168-
const opacity = Number.parseInt(opacityMatch[2], 10);
169-
170-
// Validate opacity range (0-100)
171-
if (opacity < 0 || opacity > 100) {
172-
return null;
173-
}
174-
175-
// Try arbitrary color first: [#ff0000]/50
176-
const arbitraryColor = parseArbitraryColor(baseColorKey);
177-
if (arbitraryColor !== null) {
178-
return applyOpacity(arbitraryColor, opacity);
179-
}
180-
181-
// Try preset/custom colors: red-500/50
182-
const color = getColor(baseColorKey);
183-
if (color) {
184-
return applyOpacity(color, opacity);
185-
}
186-
187-
return null;
188-
}
189-
190-
// No opacity modifier
191-
// Try arbitrary value first: [#ff0000]
192-
const arbitraryColor = parseArbitraryColor(colorKey);
193-
if (arbitraryColor !== null) {
194-
return arbitraryColor;
195-
}
196-
197-
// Try preset/custom colors: red-500
198-
return getColor(colorKey) ?? null;
199-
}
200-
20199
// Export shadow scale and colors for testing/advanced usage
202100
export { SHADOW_SCALE, COLORS as SHADOW_COLORS };

0 commit comments

Comments
 (0)