@mgcrea/react-native-tailwind 0.15.2 → 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");});});});
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Smart merge utility for StyleObject values
3
- * Handles array properties (like transform) by concatenating instead of overwriting
3
+ * Handles transform arrays with "last wins" semantics for same transform types
4
4
  */
5
5
  import type { StyleObject } from "../types/core";
6
6
  /**
7
- * Merge two StyleObject instances, handling array properties specially
7
+ * Merge two StyleObject instances, handling transform arrays specially
8
8
  *
9
9
  * @param target - The target object to merge into (mutated)
10
10
  * @param source - The source object to merge from
@@ -16,11 +16,19 @@ import type { StyleObject } from "../types/core";
16
16
  * // => { margin: 4, padding: 8 }
17
17
  *
18
18
  * @example
19
- * // Array properties (transform) are concatenated
19
+ * // Different transform types are combined
20
20
  * mergeStyles(
21
21
  * { transform: [{ rotate: '45deg' }] },
22
22
  * { transform: [{ scale: 1.1 }] }
23
23
  * )
24
24
  * // => { transform: [{ rotate: '45deg' }, { scale: 1.1 }] }
25
+ *
26
+ * @example
27
+ * // Same transform type: last wins (Tailwind parity)
28
+ * mergeStyles(
29
+ * { transform: [{ rotate: '45deg' }] },
30
+ * { transform: [{ rotate: '90deg' }] }
31
+ * )
32
+ * // => { transform: [{ rotate: '90deg' }] }
25
33
  */
26
34
  export declare function mergeStyles(target: StyleObject, source: StyleObject): StyleObject;
@@ -1 +1 @@
1
- var _interopRequireDefault=require("@babel/runtime/helpers/interopRequireDefault");Object.defineProperty(exports,"__esModule",{value:true});exports.mergeStyles=mergeStyles;var _toConsumableArray2=_interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));var ARRAY_MERGE_PROPERTIES=new Set(["transform"]);function mergeStyles(target,source){for(var key in source){if(Object.prototype.hasOwnProperty.call(source,key)){var sourceValue=source[key];if(ARRAY_MERGE_PROPERTIES.has(key)&&Array.isArray(sourceValue)){var targetValue=target[key];if(Array.isArray(targetValue)){target[key]=[].concat((0,_toConsumableArray2.default)(targetValue),(0,_toConsumableArray2.default)(sourceValue));}else{target[key]=sourceValue;}}else{target[key]=sourceValue;}}}return target;}
1
+ var _interopRequireDefault=require("@babel/runtime/helpers/interopRequireDefault");Object.defineProperty(exports,"__esModule",{value:true});exports.mergeStyles=mergeStyles;var _toConsumableArray2=_interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));function getTransformType(transform){return Object.keys(transform)[0];}function mergeTransforms(target,source){var result=(0,_toConsumableArray2.default)(target);var _loop=function _loop(){var sourceType=getTransformType(sourceTransform);var existingIndex=result.findIndex(function(t){return getTransformType(t)===sourceType;});if(existingIndex!==-1){result[existingIndex]=sourceTransform;}else{result.push(sourceTransform);}};for(var sourceTransform of source){_loop();}return result;}function mergeStyles(target,source){for(var key in source){if(Object.prototype.hasOwnProperty.call(source,key)){var sourceValue=source[key];if(key==="transform"&&Array.isArray(sourceValue)){var targetValue=target[key];if(Array.isArray(targetValue)){target.transform=mergeTransforms(targetValue,sourceValue);}else{target[key]=sourceValue;}}else{target[key]=sourceValue;}}}return target;}
@@ -1 +1 @@
1
- var _vitest=require("vitest");var _mergeStyles=require("./mergeStyles");(0,_vitest.describe)("mergeStyles",function(){(0,_vitest.describe)("standard properties",function(){(0,_vitest.it)("should merge non-array properties like Object.assign",function(){var target={margin:4};var source={padding:8};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({margin:4,padding:8});});(0,_vitest.it)("should overwrite non-array properties",function(){var target={margin:4};var source={margin:8};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({margin:8});});(0,_vitest.it)("should handle shadowOffset as standard property (object, not array)",function(){var target={shadowOffset:{width:0,height:1}};var source={shadowOffset:{width:0,height:2}};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({shadowOffset:{width:0,height:2}});});});(0,_vitest.describe)("transform array merging",function(){(0,_vitest.it)("should concatenate transform arrays",function(){var target={transform:[{rotate:"45deg"}]};var source={transform:[{scale:1.1}]};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({transform:[{rotate:"45deg"},{scale:1.1}]});});(0,_vitest.it)("should handle multiple transforms in source",function(){var target={transform:[{rotate:"45deg"}]};var source={transform:[{scale:1.1},{translateX:10}]};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({transform:[{rotate:"45deg"},{scale:1.1},{translateX:10}]});});(0,_vitest.it)("should assign transform when target has no transform",function(){var target={margin:4};var source={transform:[{scale:1.1}]};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({margin:4,transform:[{scale:1.1}]});});(0,_vitest.it)("should handle empty target transform array",function(){var target={transform:[]};var source={transform:[{scale:1.1}]};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({transform:[{scale:1.1}]});});});(0,_vitest.describe)("mixed properties",function(){(0,_vitest.it)("should handle mix of standard and transform properties",function(){var target={margin:4,transform:[{rotate:"45deg"}]};var source={padding:8,transform:[{scale:1.1}]};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({margin:4,padding:8,transform:[{rotate:"45deg"},{scale:1.1}]});});});(0,_vitest.describe)("mutation behavior",function(){(0,_vitest.it)("should mutate and return target object",function(){var target={margin:4};var source={padding:8};var result=(0,_mergeStyles.mergeStyles)(target,source);(0,_vitest.expect)(result).toBe(target);(0,_vitest.expect)(target).toEqual({margin:4,padding:8});});});});
1
+ var _vitest=require("vitest");var _mergeStyles=require("./mergeStyles");(0,_vitest.describe)("mergeStyles",function(){(0,_vitest.describe)("standard properties",function(){(0,_vitest.it)("should merge non-array properties like Object.assign",function(){var target={margin:4};var source={padding:8};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({margin:4,padding:8});});(0,_vitest.it)("should overwrite non-array properties",function(){var target={margin:4};var source={margin:8};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({margin:8});});(0,_vitest.it)("should handle shadowOffset as standard property (object, not array)",function(){var target={shadowOffset:{width:0,height:1}};var source={shadowOffset:{width:0,height:2}};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({shadowOffset:{width:0,height:2}});});});(0,_vitest.describe)("transform array merging - different types combined",function(){(0,_vitest.it)("should combine different transform types",function(){var target={transform:[{rotate:"45deg"}]};var source={transform:[{scale:1.1}]};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({transform:[{rotate:"45deg"},{scale:1.1}]});});(0,_vitest.it)("should handle multiple transforms in source",function(){var target={transform:[{rotate:"45deg"}]};var source={transform:[{scale:1.1},{translateX:10}]};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({transform:[{rotate:"45deg"},{scale:1.1},{translateX:10}]});});(0,_vitest.it)("should assign transform when target has no transform",function(){var target={margin:4};var source={transform:[{scale:1.1}]};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({margin:4,transform:[{scale:1.1}]});});(0,_vitest.it)("should handle empty target transform array",function(){var target={transform:[]};var source={transform:[{scale:1.1}]};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({transform:[{scale:1.1}]});});});(0,_vitest.describe)("transform array merging - same type last wins (Tailwind parity)",function(){(0,_vitest.it)("should replace same transform type with last value",function(){var target={transform:[{rotate:"45deg"}]};var source={transform:[{rotate:"90deg"}]};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({transform:[{rotate:"90deg"}]});});(0,_vitest.it)("should replace same scale type with last value",function(){var target={transform:[{scale:0.5}]};var source={transform:[{scale:1.1}]};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({transform:[{scale:1.1}]});});(0,_vitest.it)("should preserve order when replacing - rotate stays in position",function(){var target={transform:[{rotate:"45deg"},{scale:1.1}]};var source={transform:[{rotate:"90deg"}]};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({transform:[{rotate:"90deg"},{scale:1.1}]});});(0,_vitest.it)("should handle mixed: replace same types, add new types",function(){var target={transform:[{rotate:"45deg"},{scale:0.5}]};var source={transform:[{scale:1.1},{translateX:10}]};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({transform:[{rotate:"45deg"},{scale:1.1},{translateX:10}]});});(0,_vitest.it)("should handle scaleX and scaleY as different types",function(){var target={transform:[{scaleX:0.5}]};var source={transform:[{scaleY:1.5}]};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({transform:[{scaleX:0.5},{scaleY:1.5}]});});});(0,_vitest.describe)("mixed properties",function(){(0,_vitest.it)("should handle mix of standard and transform properties",function(){var target={margin:4,transform:[{rotate:"45deg"}]};var source={padding:8,transform:[{scale:1.1}]};(0,_vitest.expect)((0,_mergeStyles.mergeStyles)(target,source)).toEqual({margin:4,padding:8,transform:[{rotate:"45deg"},{scale:1.1}]});});});(0,_vitest.describe)("mutation behavior",function(){(0,_vitest.it)("should mutate and return target object",function(){var target={margin:4};var source={padding:8};var result=(0,_mergeStyles.mergeStyles)(target,source);(0,_vitest.expect)(result).toBe(target);(0,_vitest.expect)(target).toEqual({margin:4,padding:8});});});});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mgcrea/react-native-tailwind",
3
- "version": "0.15.2",
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
  ];
@@ -521,6 +521,39 @@ describe("parseLayout - positioning utilities", () => {
521
521
  expect(parseLayout("left-[12.5%]")).toEqual({ left: "12.5%" });
522
522
  expect(parseLayout("left-[-20px]")).toEqual({ left: -20 });
523
523
  });
524
+
525
+ it("should parse negative positioning prefixes", () => {
526
+ // Standard spacing scale
527
+ expect(parseLayout("-top-1")).toEqual({ top: -4 });
528
+ expect(parseLayout("-top-4")).toEqual({ top: -16 });
529
+ expect(parseLayout("-right-1")).toEqual({ right: -4 });
530
+ expect(parseLayout("-right-2")).toEqual({ right: -8 });
531
+ expect(parseLayout("-bottom-4")).toEqual({ bottom: -16 });
532
+ expect(parseLayout("-bottom-8")).toEqual({ bottom: -32 });
533
+ expect(parseLayout("-left-2")).toEqual({ left: -8 });
534
+ expect(parseLayout("-left-4")).toEqual({ left: -16 });
535
+ });
536
+
537
+ it("should parse negative positioning with fractional values", () => {
538
+ expect(parseLayout("-top-0.5")).toEqual({ top: -2 });
539
+ expect(parseLayout("-right-1.5")).toEqual({ right: -6 });
540
+ expect(parseLayout("-bottom-2.5")).toEqual({ bottom: -10 });
541
+ expect(parseLayout("-left-3.5")).toEqual({ left: -14 });
542
+ });
543
+
544
+ it("should parse negative positioning with arbitrary values", () => {
545
+ expect(parseLayout("-top-[10px]")).toEqual({ top: -10 });
546
+ expect(parseLayout("-right-[20px]")).toEqual({ right: -20 });
547
+ expect(parseLayout("-bottom-[30]")).toEqual({ bottom: -30 });
548
+ expect(parseLayout("-left-[40px]")).toEqual({ left: -40 });
549
+ });
550
+
551
+ it("should parse negative positioning with arbitrary percentages", () => {
552
+ expect(parseLayout("-top-[10%]")).toEqual({ top: "-10%" });
553
+ expect(parseLayout("-right-[25%]")).toEqual({ right: "-25%" });
554
+ expect(parseLayout("-bottom-[50%]")).toEqual({ bottom: "-50%" });
555
+ expect(parseLayout("-left-[100%]")).toEqual({ left: "-100%" });
556
+ });
524
557
  });
525
558
 
526
559
  describe("parseLayout - inset utilities", () => {
@@ -791,9 +824,11 @@ describe("parseLayout - custom spacing", () => {
791
824
  expect(parseLayout("inset-e-xl", customSpacing)).toEqual({ end: 64 });
792
825
  });
793
826
 
794
- it("should support negative values with custom spacing for start/end", () => {
795
- // Note: -top-*, -left-*, -right-*, -bottom-* negative prefixes are not supported
796
- // Use arbitrary values like top-[-10px] for negative positioning
827
+ it("should support negative values with custom spacing for positioning", () => {
828
+ expect(parseLayout("-top-sm", customSpacing)).toEqual({ top: -8 });
829
+ expect(parseLayout("-right-md", customSpacing)).toEqual({ right: -16 });
830
+ expect(parseLayout("-bottom-lg", customSpacing)).toEqual({ bottom: -32 });
831
+ expect(parseLayout("-left-xl", customSpacing)).toEqual({ left: -64 });
797
832
  expect(parseLayout("-start-sm", customSpacing)).toEqual({ start: -8 });
798
833
  expect(parseLayout("-end-md", customSpacing)).toEqual({ end: -16 });
799
834
  });
@@ -260,87 +260,127 @@ export function parseLayout(cls: string, customSpacing?: Record<string, number>)
260
260
  }
261
261
  }
262
262
 
263
- // Top positioning: top-0, top-4, top-[10px], top-[50%], etc.
264
- if (cls.startsWith("top-")) {
265
- const topKey = cls.substring(4);
263
+ // Top positioning: top-0, top-4, top-[10px], top-[50%], -top-4, etc.
264
+ const topMatch = cls.match(/^(-?)top-(.+)$/);
265
+ if (topMatch) {
266
+ const [, negPrefix, topKey] = topMatch;
267
+ const isNegative = negPrefix === "-";
266
268
 
267
269
  // Auto value - return empty object (no-op, removes the property)
268
270
  if (topKey === "auto") {
269
271
  return {};
270
272
  }
271
273
 
272
- // Arbitrary values: top-[123px], top-[50%], top-[-10px]
274
+ // Arbitrary values: top-[123px], top-[50%], -top-[10px]
273
275
  const arbitraryTop = parseArbitraryInset(topKey);
274
276
  if (arbitraryTop !== null) {
277
+ if (typeof arbitraryTop === "number") {
278
+ return { top: isNegative ? -arbitraryTop : arbitraryTop };
279
+ }
280
+ // Percentage values with negative prefix
281
+ if (isNegative && arbitraryTop.endsWith("%")) {
282
+ const numValue = parseFloat(arbitraryTop);
283
+ return { top: `${-numValue}%` };
284
+ }
275
285
  return { top: arbitraryTop };
276
286
  }
277
287
 
278
288
  const topValue = insetMap[topKey];
279
289
  if (topValue !== undefined) {
280
- return { top: topValue };
290
+ return { top: isNegative ? -topValue : topValue };
281
291
  }
282
292
  }
283
293
 
284
- // Right positioning: right-0, right-4, right-[10px], right-[50%], etc.
285
- if (cls.startsWith("right-")) {
286
- const rightKey = cls.substring(6);
294
+ // Right positioning: right-0, right-4, right-[10px], right-[50%], -right-4, etc.
295
+ const rightMatch = cls.match(/^(-?)right-(.+)$/);
296
+ if (rightMatch) {
297
+ const [, negPrefix, rightKey] = rightMatch;
298
+ const isNegative = negPrefix === "-";
287
299
 
288
300
  // Auto value - return empty object (no-op, removes the property)
289
301
  if (rightKey === "auto") {
290
302
  return {};
291
303
  }
292
304
 
293
- // Arbitrary values: right-[123px], right-[50%], right-[-10px]
305
+ // Arbitrary values: right-[123px], right-[50%], -right-[10px]
294
306
  const arbitraryRight = parseArbitraryInset(rightKey);
295
307
  if (arbitraryRight !== null) {
308
+ if (typeof arbitraryRight === "number") {
309
+ return { right: isNegative ? -arbitraryRight : arbitraryRight };
310
+ }
311
+ // Percentage values with negative prefix
312
+ if (isNegative && arbitraryRight.endsWith("%")) {
313
+ const numValue = parseFloat(arbitraryRight);
314
+ return { right: `${-numValue}%` };
315
+ }
296
316
  return { right: arbitraryRight };
297
317
  }
298
318
 
299
319
  const rightValue = insetMap[rightKey];
300
320
  if (rightValue !== undefined) {
301
- return { right: rightValue };
321
+ return { right: isNegative ? -rightValue : rightValue };
302
322
  }
303
323
  }
304
324
 
305
- // Bottom positioning: bottom-0, bottom-4, bottom-[10px], bottom-[50%], etc.
306
- if (cls.startsWith("bottom-")) {
307
- const bottomKey = cls.substring(7);
325
+ // Bottom positioning: bottom-0, bottom-4, bottom-[10px], bottom-[50%], -bottom-4, etc.
326
+ const bottomMatch = cls.match(/^(-?)bottom-(.+)$/);
327
+ if (bottomMatch) {
328
+ const [, negPrefix, bottomKey] = bottomMatch;
329
+ const isNegative = negPrefix === "-";
308
330
 
309
331
  // Auto value - return empty object (no-op, removes the property)
310
332
  if (bottomKey === "auto") {
311
333
  return {};
312
334
  }
313
335
 
314
- // Arbitrary values: bottom-[123px], bottom-[50%], bottom-[-10px]
336
+ // Arbitrary values: bottom-[123px], bottom-[50%], -bottom-[10px]
315
337
  const arbitraryBottom = parseArbitraryInset(bottomKey);
316
338
  if (arbitraryBottom !== null) {
339
+ if (typeof arbitraryBottom === "number") {
340
+ return { bottom: isNegative ? -arbitraryBottom : arbitraryBottom };
341
+ }
342
+ // Percentage values with negative prefix
343
+ if (isNegative && arbitraryBottom.endsWith("%")) {
344
+ const numValue = parseFloat(arbitraryBottom);
345
+ return { bottom: `${-numValue}%` };
346
+ }
317
347
  return { bottom: arbitraryBottom };
318
348
  }
319
349
 
320
350
  const bottomValue = insetMap[bottomKey];
321
351
  if (bottomValue !== undefined) {
322
- return { bottom: bottomValue };
352
+ return { bottom: isNegative ? -bottomValue : bottomValue };
323
353
  }
324
354
  }
325
355
 
326
- // Left positioning: left-0, left-4, left-[10px], left-[50%], etc.
327
- if (cls.startsWith("left-")) {
328
- const leftKey = cls.substring(5);
356
+ // Left positioning: left-0, left-4, left-[10px], left-[50%], -left-4, etc.
357
+ const leftMatch = cls.match(/^(-?)left-(.+)$/);
358
+ if (leftMatch) {
359
+ const [, negPrefix, leftKey] = leftMatch;
360
+ const isNegative = negPrefix === "-";
329
361
 
330
362
  // Auto value - return empty object (no-op, removes the property)
331
363
  if (leftKey === "auto") {
332
364
  return {};
333
365
  }
334
366
 
335
- // Arbitrary values: left-[123px], left-[50%], left-[-10px]
367
+ // Arbitrary values: left-[123px], left-[50%], -left-[10px]
336
368
  const arbitraryLeft = parseArbitraryInset(leftKey);
337
369
  if (arbitraryLeft !== null) {
370
+ if (typeof arbitraryLeft === "number") {
371
+ return { left: isNegative ? -arbitraryLeft : arbitraryLeft };
372
+ }
373
+ // Percentage values with negative prefix
374
+ if (isNegative && arbitraryLeft.endsWith("%")) {
375
+ const numValue = parseFloat(arbitraryLeft);
376
+ return { left: `${-numValue}%` };
377
+ }
338
378
  return { left: arbitraryLeft };
339
379
  }
340
380
 
341
381
  const leftValue = insetMap[leftKey];
342
382
  if (leftValue !== undefined) {
343
- return { left: leftValue };
383
+ return { left: isNegative ? -leftValue : leftValue };
344
384
  }
345
385
  }
346
386
 
@@ -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
+ });