@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.
- package/README.md +5 -0
- package/dist/babel/index.cjs +111 -25
- package/dist/parser/colors.d.ts +2 -1
- package/dist/parser/colors.js +1 -1
- package/dist/parser/colors.test.js +1 -1
- package/dist/parser/index.js +1 -1
- package/dist/parser/layout.js +1 -1
- package/dist/parser/layout.test.js +1 -1
- package/dist/parser/shadows.d.ts +7 -2
- package/dist/parser/shadows.js +1 -1
- package/dist/parser/shadows.test.js +1 -1
- package/dist/parser/transforms.test.js +1 -1
- package/dist/runtime.cjs +1 -1
- package/dist/runtime.cjs.map +4 -4
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +4 -4
- package/dist/utils/colorUtils.d.ts +30 -0
- package/dist/utils/colorUtils.js +1 -0
- package/dist/utils/colorUtils.test.js +1 -0
- package/dist/utils/mergeStyles.d.ts +11 -3
- package/dist/utils/mergeStyles.js +1 -1
- package/dist/utils/mergeStyles.test.js +1 -1
- package/package.json +1 -1
- package/src/parser/colors.test.ts +1 -16
- package/src/parser/colors.ts +15 -77
- package/src/parser/index.ts +1 -1
- package/src/parser/layout.test.ts +38 -3
- package/src/parser/layout.ts +60 -20
- package/src/parser/shadows.test.ts +109 -2
- package/src/parser/shadows.ts +20 -4
- package/src/parser/transforms.test.ts +22 -0
- package/src/utils/colorUtils.test.ts +193 -0
- package/src/utils/colorUtils.ts +115 -0
- package/src/utils/mergeStyles.test.ts +44 -2
- package/src/utils/mergeStyles.ts +58 -11
package/src/parser/shadows.ts
CHANGED
|
@@ -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
|
|
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
|
|
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" }] };
|
package/src/utils/mergeStyles.ts
CHANGED
|
@@ -1,17 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Smart merge utility for StyleObject values
|
|
3
|
-
* Handles
|
|
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
|
-
*
|
|
9
|
+
* Get the transform type key from a transform object
|
|
10
|
+
* e.g., { rotate: '45deg' } -> 'rotate', { scale: 1.1 } -> 'scale'
|
|
10
11
|
*/
|
|
11
|
-
|
|
12
|
+
function getTransformType(transform: TransformStyle): string {
|
|
13
|
+
return Object.keys(transform)[0];
|
|
14
|
+
}
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
|
-
* Merge
|
|
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
|
-
* //
|
|
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
|
|
39
|
-
if (
|
|
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
|
-
//
|
|
43
|
-
|
|
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-
|
|
96
|
+
// Standard Object.assign behavior for non-transform properties
|
|
50
97
|
target[key] = sourceValue;
|
|
51
98
|
}
|
|
52
99
|
}
|