@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.
@@ -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 };
@@ -455,4 +455,26 @@ describe("parseClassName - multiple transforms", () => {
455
455
  transform: [{ rotate: "37deg" }, { scale: 0.2 }, { translateX: 50 }],
456
456
  });
457
457
  });
458
+
459
+ // "Last wins" behavior for same transform type (Tailwind parity)
460
+ it("should use last value for duplicate rotate (Tailwind parity)", () => {
461
+ const result = parseClassName("rotate-45 rotate-90");
462
+ expect(result).toEqual({
463
+ transform: [{ rotate: "90deg" }],
464
+ });
465
+ });
466
+
467
+ it("should use last value for duplicate scale (Tailwind parity)", () => {
468
+ const result = parseClassName("scale-50 scale-110");
469
+ expect(result).toEqual({
470
+ transform: [{ scale: 1.1 }],
471
+ });
472
+ });
473
+
474
+ it("should preserve different types while replacing duplicates", () => {
475
+ const result = parseClassName("rotate-45 scale-110 rotate-90");
476
+ expect(result).toEqual({
477
+ transform: [{ rotate: "90deg" }, { scale: 1.1 }],
478
+ });
479
+ });
458
480
  });
@@ -0,0 +1,193 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { COLORS, applyOpacity, parseArbitraryColor, parseColorValue } from "./colorUtils";
3
+
4
+ describe("COLORS", () => {
5
+ it("should include basic colors", () => {
6
+ expect(COLORS.white).toBe("#FFFFFF");
7
+ expect(COLORS.black).toBe("#000000");
8
+ expect(COLORS.transparent).toBe("transparent");
9
+ });
10
+
11
+ it("should include flattened Tailwind colors", () => {
12
+ expect(COLORS["red-500"]).toBeDefined();
13
+ expect(COLORS["blue-500"]).toBeDefined();
14
+ expect(COLORS["green-500"]).toBeDefined();
15
+ expect(COLORS["gray-100"]).toBeDefined();
16
+ expect(COLORS["gray-900"]).toBeDefined();
17
+ });
18
+ });
19
+
20
+ describe("applyOpacity", () => {
21
+ it("should apply opacity to 6-digit hex colors", () => {
22
+ expect(applyOpacity("#ff0000", 50)).toBe("#FF000080");
23
+ expect(applyOpacity("#00ff00", 100)).toBe("#00FF00FF");
24
+ expect(applyOpacity("#0000ff", 0)).toBe("#0000FF00");
25
+ });
26
+
27
+ it("should apply opacity to 3-digit hex colors", () => {
28
+ expect(applyOpacity("#f00", 50)).toBe("#FF000080");
29
+ expect(applyOpacity("#0f0", 25)).toBe("#00FF0040");
30
+ expect(applyOpacity("#00f", 75)).toBe("#0000FFBF");
31
+ });
32
+
33
+ it("should handle various opacity values", () => {
34
+ expect(applyOpacity("#000000", 0)).toBe("#00000000");
35
+ expect(applyOpacity("#000000", 25)).toBe("#00000040");
36
+ expect(applyOpacity("#000000", 50)).toBe("#00000080");
37
+ expect(applyOpacity("#000000", 75)).toBe("#000000BF");
38
+ expect(applyOpacity("#000000", 80)).toBe("#000000CC");
39
+ expect(applyOpacity("#000000", 100)).toBe("#000000FF");
40
+ });
41
+
42
+ it("should return transparent unchanged", () => {
43
+ expect(applyOpacity("transparent", 50)).toBe("transparent");
44
+ expect(applyOpacity("transparent", 0)).toBe("transparent");
45
+ expect(applyOpacity("transparent", 100)).toBe("transparent");
46
+ });
47
+
48
+ it("should uppercase the output", () => {
49
+ expect(applyOpacity("#aabbcc", 50)).toBe("#AABBCC80");
50
+ expect(applyOpacity("#AbCdEf", 50)).toBe("#ABCDEF80");
51
+ });
52
+ });
53
+
54
+ describe("parseArbitraryColor", () => {
55
+ it("should parse 6-digit hex colors", () => {
56
+ expect(parseArbitraryColor("[#ff0000]")).toBe("#ff0000");
57
+ expect(parseArbitraryColor("[#00ff00]")).toBe("#00ff00");
58
+ expect(parseArbitraryColor("[#0000ff]")).toBe("#0000ff");
59
+ expect(parseArbitraryColor("[#AABBCC]")).toBe("#AABBCC");
60
+ });
61
+
62
+ it("should parse and expand 3-digit hex colors", () => {
63
+ expect(parseArbitraryColor("[#f00]")).toBe("#ff0000");
64
+ expect(parseArbitraryColor("[#0f0]")).toBe("#00ff00");
65
+ expect(parseArbitraryColor("[#00f]")).toBe("#0000ff");
66
+ expect(parseArbitraryColor("[#ABC]")).toBe("#AABBCC");
67
+ });
68
+
69
+ it("should parse 8-digit hex colors (with alpha)", () => {
70
+ expect(parseArbitraryColor("[#ff000080]")).toBe("#ff000080");
71
+ expect(parseArbitraryColor("[#00ff00cc]")).toBe("#00ff00cc");
72
+ expect(parseArbitraryColor("[#AABBCCDD]")).toBe("#AABBCCDD");
73
+ });
74
+
75
+ it("should preserve input case", () => {
76
+ expect(parseArbitraryColor("[#aabbcc]")).toBe("#aabbcc");
77
+ expect(parseArbitraryColor("[#AABBCC]")).toBe("#AABBCC");
78
+ expect(parseArbitraryColor("[#AaBbCc]")).toBe("#AaBbCc");
79
+ });
80
+
81
+ it("should return null for invalid formats", () => {
82
+ expect(parseArbitraryColor("[#gg0000]")).toBeNull();
83
+ expect(parseArbitraryColor("[#ff00]")).toBeNull();
84
+ expect(parseArbitraryColor("[#ff00000]")).toBeNull();
85
+ expect(parseArbitraryColor("[ff0000]")).toBeNull();
86
+ expect(parseArbitraryColor("#ff0000")).toBeNull();
87
+ expect(parseArbitraryColor("[rgb(255,0,0)]")).toBeNull();
88
+ expect(parseArbitraryColor("")).toBeNull();
89
+ expect(parseArbitraryColor("[]")).toBeNull();
90
+ });
91
+ });
92
+
93
+ describe("parseColorValue", () => {
94
+ describe("preset colors", () => {
95
+ it("should parse preset color names", () => {
96
+ expect(parseColorValue("red-500")).toBe(COLORS["red-500"]);
97
+ expect(parseColorValue("blue-800")).toBe(COLORS["blue-800"]);
98
+ expect(parseColorValue("green-600")).toBe(COLORS["green-600"]);
99
+ });
100
+
101
+ it("should parse basic colors", () => {
102
+ expect(parseColorValue("black")).toBe("#000000");
103
+ expect(parseColorValue("white")).toBe("#FFFFFF");
104
+ expect(parseColorValue("transparent")).toBe("transparent");
105
+ });
106
+
107
+ it("should return null for invalid color names", () => {
108
+ expect(parseColorValue("notacolor")).toBeNull();
109
+ expect(parseColorValue("red-999")).toBeNull();
110
+ expect(parseColorValue("foobar-500")).toBeNull();
111
+ });
112
+ });
113
+
114
+ describe("arbitrary colors", () => {
115
+ it("should parse arbitrary hex colors", () => {
116
+ expect(parseColorValue("[#ff0000]")).toBe("#ff0000");
117
+ expect(parseColorValue("[#00ff00]")).toBe("#00ff00");
118
+ expect(parseColorValue("[#0000ff]")).toBe("#0000ff");
119
+ });
120
+
121
+ it("should parse 3-digit arbitrary hex colors", () => {
122
+ expect(parseColorValue("[#f00]")).toBe("#ff0000");
123
+ expect(parseColorValue("[#0f0]")).toBe("#00ff00");
124
+ expect(parseColorValue("[#00f]")).toBe("#0000ff");
125
+ });
126
+
127
+ it("should parse 8-digit arbitrary hex colors", () => {
128
+ expect(parseColorValue("[#ff000080]")).toBe("#ff000080");
129
+ expect(parseColorValue("[#00ff00cc]")).toBe("#00ff00cc");
130
+ });
131
+ });
132
+
133
+ describe("opacity modifier", () => {
134
+ it("should apply opacity to preset colors", () => {
135
+ expect(parseColorValue("red-500/50")).toBe(applyOpacity(COLORS["red-500"], 50));
136
+ expect(parseColorValue("blue-800/80")).toBe(applyOpacity(COLORS["blue-800"], 80));
137
+ expect(parseColorValue("black/25")).toBe(applyOpacity("#000000", 25));
138
+ });
139
+
140
+ it("should apply opacity to arbitrary colors", () => {
141
+ expect(parseColorValue("[#ff0000]/50")).toBe("#FF000080");
142
+ expect(parseColorValue("[#00ff00]/25")).toBe("#00FF0040");
143
+ expect(parseColorValue("[#0000ff]/80")).toBe("#0000FFCC");
144
+ });
145
+
146
+ it("should handle edge opacity values", () => {
147
+ expect(parseColorValue("red-500/0")).toBe(applyOpacity(COLORS["red-500"], 0));
148
+ expect(parseColorValue("red-500/100")).toBe(applyOpacity(COLORS["red-500"], 100));
149
+ });
150
+
151
+ it("should return null for invalid opacity values", () => {
152
+ expect(parseColorValue("red-500/101")).toBeNull();
153
+ expect(parseColorValue("red-500/-1")).toBeNull();
154
+ expect(parseColorValue("red-500/abc")).toBeNull();
155
+ });
156
+
157
+ it("should keep transparent unchanged with opacity", () => {
158
+ expect(parseColorValue("transparent/50")).toBe("transparent");
159
+ expect(parseColorValue("transparent/0")).toBe("transparent");
160
+ });
161
+ });
162
+
163
+ describe("custom colors", () => {
164
+ const customColors = {
165
+ brand: "#FF5733",
166
+ "brand-primary": "#3498DB",
167
+ "brand-secondary": "#2ECC71",
168
+ };
169
+
170
+ it("should parse custom colors", () => {
171
+ expect(parseColorValue("brand", customColors)).toBe("#FF5733");
172
+ expect(parseColorValue("brand-primary", customColors)).toBe("#3498DB");
173
+ expect(parseColorValue("brand-secondary", customColors)).toBe("#2ECC71");
174
+ });
175
+
176
+ it("should apply opacity to custom colors", () => {
177
+ expect(parseColorValue("brand/50", customColors)).toBe("#FF573380");
178
+ expect(parseColorValue("brand-primary/80", customColors)).toBe("#3498DBCC");
179
+ });
180
+
181
+ it("should still support preset colors with custom colors", () => {
182
+ expect(parseColorValue("red-500", customColors)).toBe(COLORS["red-500"]);
183
+ expect(parseColorValue("blue-800/50", customColors)).toBe(applyOpacity(COLORS["blue-800"], 50));
184
+ });
185
+
186
+ it("should allow custom colors to override presets", () => {
187
+ const overrideColors = {
188
+ "red-500": "#CUSTOM1",
189
+ };
190
+ expect(parseColorValue("red-500", overrideColors)).toBe("#CUSTOM1");
191
+ });
192
+ });
193
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Shared color utilities for parsing and manipulating colors
3
+ */
4
+
5
+ import { TAILWIND_COLORS } from "../config/tailwind";
6
+ import { flattenColors } from "./flattenColors";
7
+
8
+ /**
9
+ * Tailwind color palette (flattened from config) with basic colors
10
+ */
11
+ export const COLORS: Record<string, string> = {
12
+ ...flattenColors(TAILWIND_COLORS),
13
+ white: "#FFFFFF",
14
+ black: "#000000",
15
+ transparent: "transparent",
16
+ };
17
+
18
+ /**
19
+ * Apply opacity to hex color by appending alpha channel
20
+ * @param hex - Hex color string (e.g., "#ff0000", "#f00", or "transparent")
21
+ * @param opacity - Opacity value 0-100 (e.g., 50 for 50%)
22
+ * @returns 8-digit hex with alpha (e.g., "#FF000080") or transparent
23
+ */
24
+ export function applyOpacity(hex: string, opacity: number): string {
25
+ if (hex === "transparent") {
26
+ return "transparent";
27
+ }
28
+
29
+ const cleanHex = hex.replace(/^#/, "");
30
+ const fullHex =
31
+ cleanHex.length === 3
32
+ ? cleanHex
33
+ .split("")
34
+ .map((char) => char + char)
35
+ .join("")
36
+ : cleanHex;
37
+
38
+ const alpha = Math.round((opacity / 100) * 255);
39
+ const alphaHex = alpha.toString(16).padStart(2, "0").toUpperCase();
40
+
41
+ return `#${fullHex.toUpperCase()}${alphaHex}`;
42
+ }
43
+
44
+ /**
45
+ * Parse arbitrary color value: [#ff0000], [#f00], [#FF0000AA]
46
+ * Supports 3-digit, 6-digit, and 8-digit (with alpha) hex colors
47
+ * @param value - Arbitrary value string like "[#ff0000]"
48
+ * @returns Hex string if valid, null otherwise (preserves input case)
49
+ */
50
+ export function parseArbitraryColor(value: string): string | null {
51
+ const hexMatch = value.match(/^\[#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\]$/);
52
+ if (hexMatch) {
53
+ const hex = hexMatch[1];
54
+ if (hex.length === 3) {
55
+ // Expand 3-digit hex to 6-digit: #abc -> #aabbcc (preserve case)
56
+ const expanded = hex
57
+ .split("")
58
+ .map((char) => char + char)
59
+ .join("");
60
+ return `#${expanded}`;
61
+ }
62
+ return `#${hex}`;
63
+ }
64
+ return null;
65
+ }
66
+
67
+ /**
68
+ * Parse a color value with optional opacity modifier
69
+ * Handles preset colors, custom colors, arbitrary hex values, and opacity modifiers
70
+ *
71
+ * @param colorKey - Color key like "red-500", "red-500/50", "[#ff0000]", "[#ff0000]/80"
72
+ * @param customColors - Optional custom colors from tailwind.config
73
+ * @returns Hex color string or null if invalid
74
+ */
75
+ export function parseColorValue(colorKey: string, customColors?: Record<string, string>): string | null {
76
+ const getColor = (key: string): string | undefined => {
77
+ return customColors?.[key] ?? COLORS[key];
78
+ };
79
+
80
+ // Check for opacity modifier: red-500/50, [#ff0000]/80
81
+ const opacityMatch = colorKey.match(/^(.+)\/(\d+)$/);
82
+ if (opacityMatch) {
83
+ const baseColorKey = opacityMatch[1];
84
+ const opacity = Number.parseInt(opacityMatch[2], 10);
85
+
86
+ // Validate opacity range (0-100)
87
+ if (opacity < 0 || opacity > 100) {
88
+ return null;
89
+ }
90
+
91
+ // Try arbitrary color first: [#ff0000]/50
92
+ const arbitraryColor = parseArbitraryColor(baseColorKey);
93
+ if (arbitraryColor !== null) {
94
+ return applyOpacity(arbitraryColor, opacity);
95
+ }
96
+
97
+ // Try preset/custom colors: red-500/50
98
+ const color = getColor(baseColorKey);
99
+ if (color) {
100
+ return applyOpacity(color, opacity);
101
+ }
102
+
103
+ return null;
104
+ }
105
+
106
+ // No opacity modifier
107
+ // Try arbitrary value first: [#ff0000]
108
+ const arbitraryColor = parseArbitraryColor(colorKey);
109
+ if (arbitraryColor !== null) {
110
+ return arbitraryColor;
111
+ }
112
+
113
+ // Try preset/custom colors: red-500
114
+ return getColor(colorKey) ?? null;
115
+ }
@@ -24,8 +24,8 @@ describe("mergeStyles", () => {
24
24
  });
25
25
  });
26
26
 
27
- describe("transform array merging", () => {
28
- it("should concatenate transform arrays", () => {
27
+ describe("transform array merging - different types combined", () => {
28
+ it("should combine different transform types", () => {
29
29
  const target = { transform: [{ rotate: "45deg" }] };
30
30
  const source = { transform: [{ scale: 1.1 }] };
31
31
  expect(mergeStyles(target, source)).toEqual({
@@ -59,6 +59,48 @@ describe("mergeStyles", () => {
59
59
  });
60
60
  });
61
61
 
62
+ describe("transform array merging - same type last wins (Tailwind parity)", () => {
63
+ it("should replace same transform type with last value", () => {
64
+ const target = { transform: [{ rotate: "45deg" }] };
65
+ const source = { transform: [{ rotate: "90deg" }] };
66
+ expect(mergeStyles(target, source)).toEqual({
67
+ transform: [{ rotate: "90deg" }],
68
+ });
69
+ });
70
+
71
+ it("should replace same scale type with last value", () => {
72
+ const target = { transform: [{ scale: 0.5 }] };
73
+ const source = { transform: [{ scale: 1.1 }] };
74
+ expect(mergeStyles(target, source)).toEqual({
75
+ transform: [{ scale: 1.1 }],
76
+ });
77
+ });
78
+
79
+ it("should preserve order when replacing - rotate stays in position", () => {
80
+ const target = { transform: [{ rotate: "45deg" }, { scale: 1.1 }] };
81
+ const source = { transform: [{ rotate: "90deg" }] };
82
+ expect(mergeStyles(target, source)).toEqual({
83
+ transform: [{ rotate: "90deg" }, { scale: 1.1 }],
84
+ });
85
+ });
86
+
87
+ it("should handle mixed: replace same types, add new types", () => {
88
+ const target = { transform: [{ rotate: "45deg" }, { scale: 0.5 }] };
89
+ const source = { transform: [{ scale: 1.1 }, { translateX: 10 }] };
90
+ expect(mergeStyles(target, source)).toEqual({
91
+ transform: [{ rotate: "45deg" }, { scale: 1.1 }, { translateX: 10 }],
92
+ });
93
+ });
94
+
95
+ it("should handle scaleX and scaleY as different types", () => {
96
+ const target = { transform: [{ scaleX: 0.5 }] };
97
+ const source = { transform: [{ scaleY: 1.5 }] };
98
+ expect(mergeStyles(target, source)).toEqual({
99
+ transform: [{ scaleX: 0.5 }, { scaleY: 1.5 }],
100
+ });
101
+ });
102
+ });
103
+
62
104
  describe("mixed properties", () => {
63
105
  it("should handle mix of standard and transform properties", () => {
64
106
  const target = { margin: 4, transform: [{ rotate: "45deg" }] };
@@ -1,17 +1,56 @@
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
 
6
- import type { StyleObject } from "../types/core";
6
+ import type { StyleObject, TransformStyle } from "../types/core";
7
7
 
8
8
  /**
9
- * Properties that should be merged as arrays (concatenated) rather than overwritten
9
+ * Get the transform type key from a transform object
10
+ * e.g., { rotate: '45deg' } -> 'rotate', { scale: 1.1 } -> 'scale'
10
11
  */
11
- const ARRAY_MERGE_PROPERTIES = new Set<string>(["transform"]);
12
+ function getTransformType(transform: TransformStyle): string {
13
+ return Object.keys(transform)[0];
14
+ }
12
15
 
13
16
  /**
14
- * Merge two StyleObject instances, handling array properties specially
17
+ * Merge transform arrays with "last wins" semantics for duplicate transform types.
18
+ * Different transform types are combined, but if the same type appears twice,
19
+ * the later one replaces the earlier one (matching Tailwind CSS behavior).
20
+ *
21
+ * @example
22
+ * // Different types are combined
23
+ * mergeTransforms([{ rotate: '45deg' }], [{ scale: 1.1 }])
24
+ * // => [{ rotate: '45deg' }, { scale: 1.1 }]
25
+ *
26
+ * @example
27
+ * // Same type: last wins
28
+ * mergeTransforms([{ rotate: '45deg' }], [{ rotate: '90deg' }])
29
+ * // => [{ rotate: '90deg' }]
30
+ */
31
+ function mergeTransforms(target: TransformStyle[], source: TransformStyle[]): TransformStyle[] {
32
+ // Build result by processing target first, then source
33
+ // For each source transform, replace any existing transform of the same type
34
+ const result: TransformStyle[] = [...target];
35
+
36
+ for (const sourceTransform of source) {
37
+ const sourceType = getTransformType(sourceTransform);
38
+ const existingIndex = result.findIndex((t) => getTransformType(t) === sourceType);
39
+
40
+ if (existingIndex !== -1) {
41
+ // Replace existing transform of same type (last wins)
42
+ result[existingIndex] = sourceTransform;
43
+ } else {
44
+ // Add new transform type
45
+ result.push(sourceTransform);
46
+ }
47
+ }
48
+
49
+ return result;
50
+ }
51
+
52
+ /**
53
+ * Merge two StyleObject instances, handling transform arrays specially
15
54
  *
16
55
  * @param target - The target object to merge into (mutated)
17
56
  * @param source - The source object to merge from
@@ -23,30 +62,38 @@ const ARRAY_MERGE_PROPERTIES = new Set<string>(["transform"]);
23
62
  * // => { margin: 4, padding: 8 }
24
63
  *
25
64
  * @example
26
- * // Array properties (transform) are concatenated
65
+ * // Different transform types are combined
27
66
  * mergeStyles(
28
67
  * { transform: [{ rotate: '45deg' }] },
29
68
  * { transform: [{ scale: 1.1 }] }
30
69
  * )
31
70
  * // => { transform: [{ rotate: '45deg' }, { scale: 1.1 }] }
71
+ *
72
+ * @example
73
+ * // Same transform type: last wins (Tailwind parity)
74
+ * mergeStyles(
75
+ * { transform: [{ rotate: '45deg' }] },
76
+ * { transform: [{ rotate: '90deg' }] }
77
+ * )
78
+ * // => { transform: [{ rotate: '90deg' }] }
32
79
  */
33
80
  export function mergeStyles(target: StyleObject, source: StyleObject): StyleObject {
34
81
  for (const key in source) {
35
82
  if (Object.prototype.hasOwnProperty.call(source, key)) {
36
83
  const sourceValue = source[key];
37
84
 
38
- // Handle array merge properties (like transform)
39
- if (ARRAY_MERGE_PROPERTIES.has(key) && Array.isArray(sourceValue)) {
85
+ // Handle transform arrays specially
86
+ if (key === "transform" && Array.isArray(sourceValue)) {
40
87
  const targetValue = target[key];
41
88
  if (Array.isArray(targetValue)) {
42
- // Concatenate arrays
43
- (target as Record<string, unknown>)[key] = [...targetValue, ...sourceValue];
89
+ // Merge transforms with "last wins" for same types
90
+ target.transform = mergeTransforms(targetValue, sourceValue);
44
91
  } else {
45
92
  // No existing array, just assign
46
93
  target[key] = sourceValue;
47
94
  }
48
95
  } else {
49
- // Standard Object.assign behavior for non-array properties
96
+ // Standard Object.assign behavior for non-transform properties
50
97
  target[key] = sourceValue;
51
98
  }
52
99
  }