@mgcrea/react-native-tailwind 0.15.3 → 0.15.4

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,30 @@
1
+ /**
2
+ * Shared color utilities for parsing and manipulating colors
3
+ */
4
+ /**
5
+ * Tailwind color palette (flattened from config) with basic colors
6
+ */
7
+ export declare const COLORS: Record<string, string>;
8
+ /**
9
+ * Apply opacity to hex color by appending alpha channel
10
+ * @param hex - Hex color string (e.g., "#ff0000", "#f00", or "transparent")
11
+ * @param opacity - Opacity value 0-100 (e.g., 50 for 50%)
12
+ * @returns 8-digit hex with alpha (e.g., "#FF000080") or transparent
13
+ */
14
+ export declare function applyOpacity(hex: string, opacity: number): string;
15
+ /**
16
+ * Parse arbitrary color value: [#ff0000], [#f00], [#FF0000AA]
17
+ * Supports 3-digit, 6-digit, and 8-digit (with alpha) hex colors
18
+ * @param value - Arbitrary value string like "[#ff0000]"
19
+ * @returns Hex string if valid, null otherwise (preserves input case)
20
+ */
21
+ export declare function parseArbitraryColor(value: string): string | null;
22
+ /**
23
+ * Parse a color value with optional opacity modifier
24
+ * Handles preset colors, custom colors, arbitrary hex values, and opacity modifiers
25
+ *
26
+ * @param colorKey - Color key like "red-500", "red-500/50", "[#ff0000]", "[#ff0000]/80"
27
+ * @param customColors - Optional custom colors from tailwind.config
28
+ * @returns Hex color string or null if invalid
29
+ */
30
+ export declare function parseColorValue(colorKey: string, customColors?: Record<string, string>): string | null;
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,"__esModule",{value:true});exports.COLORS=void 0;exports.applyOpacity=applyOpacity;exports.parseArbitraryColor=parseArbitraryColor;exports.parseColorValue=parseColorValue;var _tailwind=require("../config/tailwind");var _flattenColors=require("./flattenColors");var COLORS=exports.COLORS=Object.assign({},(0,_flattenColors.flattenColors)(_tailwind.TAILWIND_COLORS),{white:"#FFFFFF",black:"#000000",transparent:"transparent"});function applyOpacity(hex,opacity){if(hex==="transparent"){return"transparent";}var cleanHex=hex.replace(/^#/,"");var fullHex=cleanHex.length===3?cleanHex.split("").map(function(char){return char+char;}).join(""):cleanHex;var alpha=Math.round(opacity/100*255);var alphaHex=alpha.toString(16).padStart(2,"0").toUpperCase();return`#${fullHex.toUpperCase()}${alphaHex}`;}function parseArbitraryColor(value){var hexMatch=value.match(/^\[#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\]$/);if(hexMatch){var hex=hexMatch[1];if(hex.length===3){var expanded=hex.split("").map(function(char){return char+char;}).join("");return`#${expanded}`;}return`#${hex}`;}return null;}function parseColorValue(colorKey,customColors){var _getColor;var getColor=function getColor(key){var _customColors$key;return(_customColors$key=customColors==null?void 0:customColors[key])!=null?_customColors$key:COLORS[key];};var opacityMatch=colorKey.match(/^(.+)\/(\d+)$/);if(opacityMatch){var baseColorKey=opacityMatch[1];var opacity=Number.parseInt(opacityMatch[2],10);if(opacity<0||opacity>100){return null;}var _arbitraryColor=parseArbitraryColor(baseColorKey);if(_arbitraryColor!==null){return applyOpacity(_arbitraryColor,opacity);}var color=getColor(baseColorKey);if(color){return applyOpacity(color,opacity);}return null;}var arbitraryColor=parseArbitraryColor(colorKey);if(arbitraryColor!==null){return arbitraryColor;}return(_getColor=getColor(colorKey))!=null?_getColor:null;}
@@ -0,0 +1 @@
1
+ var _vitest=require("vitest");var _colorUtils=require("./colorUtils");(0,_vitest.describe)("COLORS",function(){(0,_vitest.it)("should include basic colors",function(){(0,_vitest.expect)(_colorUtils.COLORS.white).toBe("#FFFFFF");(0,_vitest.expect)(_colorUtils.COLORS.black).toBe("#000000");(0,_vitest.expect)(_colorUtils.COLORS.transparent).toBe("transparent");});(0,_vitest.it)("should include flattened Tailwind colors",function(){(0,_vitest.expect)(_colorUtils.COLORS["red-500"]).toBeDefined();(0,_vitest.expect)(_colorUtils.COLORS["blue-500"]).toBeDefined();(0,_vitest.expect)(_colorUtils.COLORS["green-500"]).toBeDefined();(0,_vitest.expect)(_colorUtils.COLORS["gray-100"]).toBeDefined();(0,_vitest.expect)(_colorUtils.COLORS["gray-900"]).toBeDefined();});});(0,_vitest.describe)("applyOpacity",function(){(0,_vitest.it)("should apply opacity to 6-digit hex colors",function(){(0,_vitest.expect)((0,_colorUtils.applyOpacity)("#ff0000",50)).toBe("#FF000080");(0,_vitest.expect)((0,_colorUtils.applyOpacity)("#00ff00",100)).toBe("#00FF00FF");(0,_vitest.expect)((0,_colorUtils.applyOpacity)("#0000ff",0)).toBe("#0000FF00");});(0,_vitest.it)("should apply opacity to 3-digit hex colors",function(){(0,_vitest.expect)((0,_colorUtils.applyOpacity)("#f00",50)).toBe("#FF000080");(0,_vitest.expect)((0,_colorUtils.applyOpacity)("#0f0",25)).toBe("#00FF0040");(0,_vitest.expect)((0,_colorUtils.applyOpacity)("#00f",75)).toBe("#0000FFBF");});(0,_vitest.it)("should handle various opacity values",function(){(0,_vitest.expect)((0,_colorUtils.applyOpacity)("#000000",0)).toBe("#00000000");(0,_vitest.expect)((0,_colorUtils.applyOpacity)("#000000",25)).toBe("#00000040");(0,_vitest.expect)((0,_colorUtils.applyOpacity)("#000000",50)).toBe("#00000080");(0,_vitest.expect)((0,_colorUtils.applyOpacity)("#000000",75)).toBe("#000000BF");(0,_vitest.expect)((0,_colorUtils.applyOpacity)("#000000",80)).toBe("#000000CC");(0,_vitest.expect)((0,_colorUtils.applyOpacity)("#000000",100)).toBe("#000000FF");});(0,_vitest.it)("should return transparent unchanged",function(){(0,_vitest.expect)((0,_colorUtils.applyOpacity)("transparent",50)).toBe("transparent");(0,_vitest.expect)((0,_colorUtils.applyOpacity)("transparent",0)).toBe("transparent");(0,_vitest.expect)((0,_colorUtils.applyOpacity)("transparent",100)).toBe("transparent");});(0,_vitest.it)("should uppercase the output",function(){(0,_vitest.expect)((0,_colorUtils.applyOpacity)("#aabbcc",50)).toBe("#AABBCC80");(0,_vitest.expect)((0,_colorUtils.applyOpacity)("#AbCdEf",50)).toBe("#ABCDEF80");});});(0,_vitest.describe)("parseArbitraryColor",function(){(0,_vitest.it)("should parse 6-digit hex colors",function(){(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("[#ff0000]")).toBe("#ff0000");(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("[#00ff00]")).toBe("#00ff00");(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("[#0000ff]")).toBe("#0000ff");(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("[#AABBCC]")).toBe("#AABBCC");});(0,_vitest.it)("should parse and expand 3-digit hex colors",function(){(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("[#f00]")).toBe("#ff0000");(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("[#0f0]")).toBe("#00ff00");(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("[#00f]")).toBe("#0000ff");(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("[#ABC]")).toBe("#AABBCC");});(0,_vitest.it)("should parse 8-digit hex colors (with alpha)",function(){(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("[#ff000080]")).toBe("#ff000080");(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("[#00ff00cc]")).toBe("#00ff00cc");(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("[#AABBCCDD]")).toBe("#AABBCCDD");});(0,_vitest.it)("should preserve input case",function(){(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("[#aabbcc]")).toBe("#aabbcc");(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("[#AABBCC]")).toBe("#AABBCC");(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("[#AaBbCc]")).toBe("#AaBbCc");});(0,_vitest.it)("should return null for invalid formats",function(){(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("[#gg0000]")).toBeNull();(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("[#ff00]")).toBeNull();(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("[#ff00000]")).toBeNull();(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("[ff0000]")).toBeNull();(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("#ff0000")).toBeNull();(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("[rgb(255,0,0)]")).toBeNull();(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("")).toBeNull();(0,_vitest.expect)((0,_colorUtils.parseArbitraryColor)("[]")).toBeNull();});});(0,_vitest.describe)("parseColorValue",function(){(0,_vitest.describe)("preset colors",function(){(0,_vitest.it)("should parse preset color names",function(){(0,_vitest.expect)((0,_colorUtils.parseColorValue)("red-500")).toBe(_colorUtils.COLORS["red-500"]);(0,_vitest.expect)((0,_colorUtils.parseColorValue)("blue-800")).toBe(_colorUtils.COLORS["blue-800"]);(0,_vitest.expect)((0,_colorUtils.parseColorValue)("green-600")).toBe(_colorUtils.COLORS["green-600"]);});(0,_vitest.it)("should parse basic colors",function(){(0,_vitest.expect)((0,_colorUtils.parseColorValue)("black")).toBe("#000000");(0,_vitest.expect)((0,_colorUtils.parseColorValue)("white")).toBe("#FFFFFF");(0,_vitest.expect)((0,_colorUtils.parseColorValue)("transparent")).toBe("transparent");});(0,_vitest.it)("should return null for invalid color names",function(){(0,_vitest.expect)((0,_colorUtils.parseColorValue)("notacolor")).toBeNull();(0,_vitest.expect)((0,_colorUtils.parseColorValue)("red-999")).toBeNull();(0,_vitest.expect)((0,_colorUtils.parseColorValue)("foobar-500")).toBeNull();});});(0,_vitest.describe)("arbitrary colors",function(){(0,_vitest.it)("should parse arbitrary hex colors",function(){(0,_vitest.expect)((0,_colorUtils.parseColorValue)("[#ff0000]")).toBe("#ff0000");(0,_vitest.expect)((0,_colorUtils.parseColorValue)("[#00ff00]")).toBe("#00ff00");(0,_vitest.expect)((0,_colorUtils.parseColorValue)("[#0000ff]")).toBe("#0000ff");});(0,_vitest.it)("should parse 3-digit arbitrary hex colors",function(){(0,_vitest.expect)((0,_colorUtils.parseColorValue)("[#f00]")).toBe("#ff0000");(0,_vitest.expect)((0,_colorUtils.parseColorValue)("[#0f0]")).toBe("#00ff00");(0,_vitest.expect)((0,_colorUtils.parseColorValue)("[#00f]")).toBe("#0000ff");});(0,_vitest.it)("should parse 8-digit arbitrary hex colors",function(){(0,_vitest.expect)((0,_colorUtils.parseColorValue)("[#ff000080]")).toBe("#ff000080");(0,_vitest.expect)((0,_colorUtils.parseColorValue)("[#00ff00cc]")).toBe("#00ff00cc");});});(0,_vitest.describe)("opacity modifier",function(){(0,_vitest.it)("should apply opacity to preset colors",function(){(0,_vitest.expect)((0,_colorUtils.parseColorValue)("red-500/50")).toBe((0,_colorUtils.applyOpacity)(_colorUtils.COLORS["red-500"],50));(0,_vitest.expect)((0,_colorUtils.parseColorValue)("blue-800/80")).toBe((0,_colorUtils.applyOpacity)(_colorUtils.COLORS["blue-800"],80));(0,_vitest.expect)((0,_colorUtils.parseColorValue)("black/25")).toBe((0,_colorUtils.applyOpacity)("#000000",25));});(0,_vitest.it)("should apply opacity to arbitrary colors",function(){(0,_vitest.expect)((0,_colorUtils.parseColorValue)("[#ff0000]/50")).toBe("#FF000080");(0,_vitest.expect)((0,_colorUtils.parseColorValue)("[#00ff00]/25")).toBe("#00FF0040");(0,_vitest.expect)((0,_colorUtils.parseColorValue)("[#0000ff]/80")).toBe("#0000FFCC");});(0,_vitest.it)("should handle edge opacity values",function(){(0,_vitest.expect)((0,_colorUtils.parseColorValue)("red-500/0")).toBe((0,_colorUtils.applyOpacity)(_colorUtils.COLORS["red-500"],0));(0,_vitest.expect)((0,_colorUtils.parseColorValue)("red-500/100")).toBe((0,_colorUtils.applyOpacity)(_colorUtils.COLORS["red-500"],100));});(0,_vitest.it)("should return null for invalid opacity values",function(){(0,_vitest.expect)((0,_colorUtils.parseColorValue)("red-500/101")).toBeNull();(0,_vitest.expect)((0,_colorUtils.parseColorValue)("red-500/-1")).toBeNull();(0,_vitest.expect)((0,_colorUtils.parseColorValue)("red-500/abc")).toBeNull();});(0,_vitest.it)("should keep transparent unchanged with opacity",function(){(0,_vitest.expect)((0,_colorUtils.parseColorValue)("transparent/50")).toBe("transparent");(0,_vitest.expect)((0,_colorUtils.parseColorValue)("transparent/0")).toBe("transparent");});});(0,_vitest.describe)("custom colors",function(){var customColors={brand:"#FF5733","brand-primary":"#3498DB","brand-secondary":"#2ECC71"};(0,_vitest.it)("should parse custom colors",function(){(0,_vitest.expect)((0,_colorUtils.parseColorValue)("brand",customColors)).toBe("#FF5733");(0,_vitest.expect)((0,_colorUtils.parseColorValue)("brand-primary",customColors)).toBe("#3498DB");(0,_vitest.expect)((0,_colorUtils.parseColorValue)("brand-secondary",customColors)).toBe("#2ECC71");});(0,_vitest.it)("should apply opacity to custom colors",function(){(0,_vitest.expect)((0,_colorUtils.parseColorValue)("brand/50",customColors)).toBe("#FF573380");(0,_vitest.expect)((0,_colorUtils.parseColorValue)("brand-primary/80",customColors)).toBe("#3498DBCC");});(0,_vitest.it)("should still support preset colors with custom colors",function(){(0,_vitest.expect)((0,_colorUtils.parseColorValue)("red-500",customColors)).toBe(_colorUtils.COLORS["red-500"]);(0,_vitest.expect)((0,_colorUtils.parseColorValue)("blue-800/50",customColors)).toBe((0,_colorUtils.applyOpacity)(_colorUtils.COLORS["blue-800"],50));});(0,_vitest.it)("should allow custom colors to override presets",function(){var overrideColors={"red-500":"#CUSTOM1"};(0,_vitest.expect)((0,_colorUtils.parseColorValue)("red-500",overrideColors)).toBe("#CUSTOM1");});});});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mgcrea/react-native-tailwind",
3
- "version": "0.15.3",
3
+ "version": "0.15.4",
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",
@@ -1,21 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  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";
19
4
 
20
5
  describe("COLORS", () => {
21
6
  it("should export complete color palette", () => {
@@ -2,85 +2,11 @@
2
2
  * Color utilities (background, text, border colors)
3
3
  */
4
4
 
5
- import { TAILWIND_COLORS } from "../config/tailwind";
6
5
  import type { StyleObject } from "../types";
7
- import { flattenColors } from "../utils/flattenColors";
6
+ import { COLORS, applyOpacity, parseArbitraryColor } from "../utils/colorUtils";
8
7
 
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 };
84
10
 
85
11
  /**
86
12
  * Parse color classes (background, text, border)
@@ -93,6 +19,7 @@ export function parseColor(cls: string, customColors?: Record<string, string>):
93
19
  };
94
20
 
95
21
  // Helper to parse color with optional opacity modifier
22
+ // Uses internal implementation to preserve warnings for invalid arbitrary colors
96
23
  const parseColorWithOpacity = (colorKey: string): string | null => {
97
24
  // Check for opacity modifier: blue-500/50
98
25
  const opacityMatch = colorKey.match(/^(.+)\/(\d+)$/);
@@ -133,6 +60,17 @@ export function parseColor(cls: string, customColors?: Record<string, string>):
133
60
  return arbitraryColor;
134
61
  }
135
62
 
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
+
136
74
  // Try preset/custom colors
137
75
  return getColor(colorKey) ?? null;
138
76
  };
@@ -60,7 +60,7 @@ export function parseClass(cls: string, customTheme?: CustomTheme): StyleObject
60
60
  (cls: string) => parseLayout(cls, customTheme?.spacing),
61
61
  (cls: string) => parseTypography(cls, customTheme?.fontFamily, customTheme?.fontSize),
62
62
  (cls: string) => parseSizing(cls, customTheme?.spacing),
63
- parseShadow,
63
+ (cls: string) => parseShadow(cls, customTheme?.colors),
64
64
  parseAspectRatio,
65
65
  (cls: string) => parseTransform(cls, customTheme?.spacing),
66
66
  ];
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { SHADOW_SCALE, parseShadow } from "./shadows";
2
+ import { SHADOW_COLORS, SHADOW_SCALE, parseShadow } from "./shadows";
3
+ import { applyOpacity } from "../utils/colorUtils";
3
4
 
4
5
  describe("SHADOW_SCALE", () => {
5
6
  it("should export complete shadow scale", () => {
@@ -138,7 +139,7 @@ describe("parseShadow - shadow properties (Android)", () => {
138
139
 
139
140
  describe("parseShadow - invalid classes", () => {
140
141
  it("should return null for invalid shadow classes", () => {
141
- expect(parseShadow("shadow-invalid")).toBeNull();
142
+ expect(parseShadow("shadow-invalidcolor")).toBeNull();
142
143
  expect(parseShadow("shadows")).toBeNull();
143
144
  expect(parseShadow("shadow-")).toBeNull();
144
145
  expect(parseShadow("shadow-small")).toBeNull();
@@ -190,3 +191,109 @@ describe("parseShadow - comprehensive coverage", () => {
190
191
  expect(parseShadow("shadow-MD")).toBeNull();
191
192
  });
192
193
  });
194
+
195
+ describe("parseShadow - shadow colors", () => {
196
+ it("should parse shadow color with preset colors", () => {
197
+ expect(parseShadow("shadow-red-500")).toEqual({ shadowColor: SHADOW_COLORS["red-500"] });
198
+ expect(parseShadow("shadow-blue-800")).toEqual({ shadowColor: SHADOW_COLORS["blue-800"] });
199
+ expect(parseShadow("shadow-green-600")).toEqual({ shadowColor: SHADOW_COLORS["green-600"] });
200
+ });
201
+
202
+ it("should parse shadow color with basic colors", () => {
203
+ expect(parseShadow("shadow-black")).toEqual({ shadowColor: SHADOW_COLORS.black });
204
+ expect(parseShadow("shadow-white")).toEqual({ shadowColor: SHADOW_COLORS.white });
205
+ expect(parseShadow("shadow-transparent")).toEqual({ shadowColor: "transparent" });
206
+ });
207
+
208
+ it("should parse shadow color with opacity modifier", () => {
209
+ expect(parseShadow("shadow-red-500/50")).toEqual({
210
+ shadowColor: applyOpacity(SHADOW_COLORS["red-500"], 50),
211
+ });
212
+ expect(parseShadow("shadow-blue-800/80")).toEqual({
213
+ shadowColor: applyOpacity(SHADOW_COLORS["blue-800"], 80),
214
+ });
215
+ expect(parseShadow("shadow-black/25")).toEqual({
216
+ shadowColor: applyOpacity(SHADOW_COLORS.black, 25),
217
+ });
218
+ expect(parseShadow("shadow-white/0")).toEqual({
219
+ shadowColor: applyOpacity(SHADOW_COLORS.white, 0),
220
+ });
221
+ expect(parseShadow("shadow-green-500/100")).toEqual({
222
+ shadowColor: applyOpacity(SHADOW_COLORS["green-500"], 100),
223
+ });
224
+ });
225
+
226
+ it("should parse shadow color with arbitrary hex values", () => {
227
+ expect(parseShadow("shadow-[#ff0000]")).toEqual({ shadowColor: "#ff0000" });
228
+ expect(parseShadow("shadow-[#00ff00]")).toEqual({ shadowColor: "#00ff00" });
229
+ expect(parseShadow("shadow-[#0000ff]")).toEqual({ shadowColor: "#0000ff" });
230
+ });
231
+
232
+ it("should parse shadow color with 3-digit hex values", () => {
233
+ expect(parseShadow("shadow-[#f00]")).toEqual({ shadowColor: "#ff0000" });
234
+ expect(parseShadow("shadow-[#0f0]")).toEqual({ shadowColor: "#00ff00" });
235
+ expect(parseShadow("shadow-[#00f]")).toEqual({ shadowColor: "#0000ff" });
236
+ });
237
+
238
+ it("should parse shadow color with arbitrary hex and opacity", () => {
239
+ // When opacity is applied, the color is uppercased for consistency
240
+ expect(parseShadow("shadow-[#ff0000]/50")).toEqual({ shadowColor: "#FF000080" });
241
+ expect(parseShadow("shadow-[#00ff00]/25")).toEqual({ shadowColor: "#00FF0040" });
242
+ expect(parseShadow("shadow-[#0000ff]/80")).toEqual({ shadowColor: "#0000FFCC" });
243
+ });
244
+
245
+ it("should parse shadow color with 8-digit hex (with alpha)", () => {
246
+ expect(parseShadow("shadow-[#ff000080]")).toEqual({ shadowColor: "#ff000080" });
247
+ expect(parseShadow("shadow-[#00ff00cc]")).toEqual({ shadowColor: "#00ff00cc" });
248
+ });
249
+
250
+ it("should handle transparent with opacity modifier", () => {
251
+ // Transparent should remain transparent even with opacity
252
+ expect(parseShadow("shadow-transparent/50")).toEqual({ shadowColor: "transparent" });
253
+ });
254
+
255
+ it("should return null for invalid opacity values", () => {
256
+ expect(parseShadow("shadow-red-500/101")).toBeNull();
257
+ expect(parseShadow("shadow-red-500/-1")).toBeNull();
258
+ expect(parseShadow("shadow-red-500/abc")).toBeNull();
259
+ });
260
+
261
+ it("should return null for invalid color names", () => {
262
+ expect(parseShadow("shadow-notacolor")).toBeNull();
263
+ expect(parseShadow("shadow-foobar-500")).toBeNull();
264
+ expect(parseShadow("shadow-red-999")).toBeNull();
265
+ });
266
+ });
267
+
268
+ describe("parseShadow - shadow colors with custom colors", () => {
269
+ const customColors = {
270
+ brand: "#FF5733",
271
+ "brand-primary": "#3498DB",
272
+ "brand-secondary": "#2ECC71",
273
+ };
274
+
275
+ it("should parse shadow with custom colors", () => {
276
+ expect(parseShadow("shadow-brand", customColors)).toEqual({ shadowColor: "#FF5733" });
277
+ expect(parseShadow("shadow-brand-primary", customColors)).toEqual({ shadowColor: "#3498DB" });
278
+ expect(parseShadow("shadow-brand-secondary", customColors)).toEqual({ shadowColor: "#2ECC71" });
279
+ });
280
+
281
+ it("should parse shadow with custom colors and opacity", () => {
282
+ expect(parseShadow("shadow-brand/50", customColors)).toEqual({ shadowColor: "#FF573380" });
283
+ expect(parseShadow("shadow-brand-primary/80", customColors)).toEqual({ shadowColor: "#3498DBCC" });
284
+ });
285
+
286
+ it("should still support preset colors with custom colors", () => {
287
+ expect(parseShadow("shadow-red-500", customColors)).toEqual({ shadowColor: SHADOW_COLORS["red-500"] });
288
+ expect(parseShadow("shadow-blue-800/50", customColors)).toEqual({
289
+ shadowColor: applyOpacity(SHADOW_COLORS["blue-800"], 50),
290
+ });
291
+ });
292
+
293
+ it("should allow custom colors to override presets", () => {
294
+ const overrideColors = {
295
+ "red-500": "#CUSTOM1",
296
+ };
297
+ expect(parseShadow("shadow-red-500", overrideColors)).toEqual({ shadowColor: "#CUSTOM1" });
298
+ });
299
+ });
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import type { StyleObject } from "../types";
7
+ import { COLORS, parseColorValue } from "../utils/colorUtils";
7
8
 
8
9
  /**
9
10
  * Shadow scale definitions combining iOS and Android properties
@@ -68,17 +69,32 @@ const SHADOW_SCALE: Record<string, StyleObject> = {
68
69
 
69
70
  /**
70
71
  * Parse shadow classes
72
+ * Supports shadow size presets (shadow-sm, shadow-md, etc.) and
73
+ * shadow colors (shadow-red-500, shadow-blue-800/50, shadow-[#ff0000]/80)
74
+ *
71
75
  * @param cls - Class name to parse
76
+ * @param customColors - Optional custom colors from tailwind.config
72
77
  * @returns Style object or null if not a shadow class
73
78
  */
74
- export function parseShadow(cls: string): StyleObject | null {
75
- // Check if it's a shadow class
79
+ export function parseShadow(cls: string, customColors?: Record<string, string>): StyleObject | null {
80
+ // Check if it's a shadow size preset
76
81
  if (cls in SHADOW_SCALE) {
77
82
  return SHADOW_SCALE[cls];
78
83
  }
79
84
 
85
+ // Check for shadow color: shadow-red-500, shadow-red-500/50, shadow-[#ff0000]/80
86
+ if (cls.startsWith("shadow-")) {
87
+ const colorPart = cls.substring(7); // Remove "shadow-"
88
+
89
+ // Parse the color value using shared utility
90
+ const shadowColor = parseColorValue(colorPart, customColors);
91
+ if (shadowColor) {
92
+ return { shadowColor };
93
+ }
94
+ }
95
+
80
96
  return null;
81
97
  }
82
98
 
83
- // Export shadow scale for testing/advanced usage
84
- export { SHADOW_SCALE };
99
+ // Export shadow scale and colors for testing/advanced usage
100
+ export { SHADOW_SCALE, COLORS as SHADOW_COLORS };
@@ -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
+ });