@mgcrea/react-native-tailwind 0.6.0 → 0.7.0
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 +437 -10
- package/dist/babel/config-loader.ts +1 -23
- package/dist/babel/index.cjs +543 -150
- package/dist/babel/index.d.ts +27 -2
- package/dist/babel/index.test.ts +268 -0
- package/dist/babel/index.ts +352 -44
- package/dist/components/Pressable.d.ts +2 -0
- package/dist/components/TextInput.d.ts +2 -0
- package/dist/config/palettes.d.ts +302 -0
- package/dist/config/palettes.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -1
- package/dist/parser/__snapshots__/colors.test.js.snap +242 -90
- package/dist/parser/__snapshots__/transforms.test.js.snap +58 -0
- package/dist/parser/colors.js +1 -1
- package/dist/parser/colors.test.js +1 -1
- package/dist/parser/layout.js +1 -1
- package/dist/parser/layout.test.js +1 -1
- package/dist/parser/typography.js +1 -1
- package/dist/parser/typography.test.js +1 -1
- package/dist/runtime.cjs +2 -0
- package/dist/runtime.cjs.map +7 -0
- package/dist/runtime.d.ts +139 -0
- package/dist/runtime.js +2 -0
- package/dist/runtime.js.map +7 -0
- package/dist/runtime.test.js +1 -0
- package/dist/stubs/tw.d.ts +60 -0
- package/dist/stubs/tw.js +1 -0
- package/dist/utils/flattenColors.d.ts +16 -0
- package/dist/utils/flattenColors.js +1 -0
- package/dist/utils/flattenColors.test.js +1 -0
- package/dist/utils/modifiers.d.ts +29 -0
- package/dist/utils/modifiers.js +1 -0
- package/dist/utils/modifiers.test.js +1 -0
- package/dist/utils/styleKey.test.js +1 -0
- package/package.json +15 -3
- package/src/babel/config-loader.ts +1 -23
- package/src/babel/index.test.ts +268 -0
- package/src/babel/index.ts +352 -44
- package/src/components/Pressable.tsx +1 -0
- package/src/components/TextInput.tsx +1 -0
- package/src/config/palettes.ts +304 -0
- package/src/index.ts +5 -0
- package/src/parser/colors.test.ts +47 -31
- package/src/parser/colors.ts +5 -110
- package/src/parser/layout.test.ts +35 -0
- package/src/parser/layout.ts +26 -0
- package/src/parser/typography.test.ts +10 -0
- package/src/parser/typography.ts +8 -0
- package/src/runtime.test.ts +325 -0
- package/src/runtime.ts +280 -0
- package/src/stubs/tw.ts +80 -0
- package/src/utils/flattenColors.test.ts +361 -0
- package/src/utils/flattenColors.ts +32 -0
- package/src/utils/modifiers.test.ts +286 -0
- package/src/utils/modifiers.ts +63 -0
- package/src/utils/styleKey.test.ts +168 -0
|
@@ -41,6 +41,14 @@ describe("parseTypography - font size", () => {
|
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
describe("parseTypography - font family", () => {
|
|
45
|
+
it("should parse font family values", () => {
|
|
46
|
+
expect(parseTypography("font-sans")).toEqual({ fontFamily: "System" });
|
|
47
|
+
expect(parseTypography("font-serif")).toEqual({ fontFamily: "serif" });
|
|
48
|
+
expect(parseTypography("font-mono")).toEqual({ fontFamily: "Courier" });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
44
52
|
describe("parseTypography - font weight", () => {
|
|
45
53
|
it("should parse font weight values", () => {
|
|
46
54
|
expect(parseTypography("font-thin")).toEqual({ fontWeight: "100" });
|
|
@@ -177,6 +185,8 @@ describe("parseTypography - comprehensive coverage", () => {
|
|
|
177
185
|
it("should handle all typography categories independently", () => {
|
|
178
186
|
// Font size
|
|
179
187
|
expect(parseTypography("text-base")).toEqual({ fontSize: 16 });
|
|
188
|
+
// Font family
|
|
189
|
+
expect(parseTypography("font-mono")).toEqual({ fontFamily: "Courier" });
|
|
180
190
|
// Font weight
|
|
181
191
|
expect(parseTypography("font-bold")).toEqual({ fontWeight: "700" });
|
|
182
192
|
// Font style
|
package/src/parser/typography.ts
CHANGED
|
@@ -31,6 +31,13 @@ export const LETTER_SPACING_SCALE: Record<string, number> = {
|
|
|
31
31
|
widest: 1.6,
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
// Font family utilities
|
|
35
|
+
const FONT_FAMILY_MAP: Record<string, StyleObject> = {
|
|
36
|
+
"font-sans": { fontFamily: "System" },
|
|
37
|
+
"font-serif": { fontFamily: "serif" },
|
|
38
|
+
"font-mono": { fontFamily: "Courier" },
|
|
39
|
+
};
|
|
40
|
+
|
|
34
41
|
// Font weight utilities
|
|
35
42
|
const FONT_WEIGHT_MAP: Record<string, StyleObject> = {
|
|
36
43
|
"font-thin": { fontWeight: "100" },
|
|
@@ -175,6 +182,7 @@ export function parseTypography(cls: string): StyleObject | null {
|
|
|
175
182
|
|
|
176
183
|
// Try each lookup table in order
|
|
177
184
|
return (
|
|
185
|
+
FONT_FAMILY_MAP[cls] ??
|
|
178
186
|
FONT_WEIGHT_MAP[cls] ??
|
|
179
187
|
FONT_STYLE_MAP[cls] ??
|
|
180
188
|
TEXT_ALIGN_MAP[cls] ??
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { clearCache, getCacheStats, getCustomColors, setConfig, tw, twStyle } from "./runtime";
|
|
3
|
+
|
|
4
|
+
describe("runtime", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
clearCache();
|
|
7
|
+
setConfig({}); // Reset config
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe("tw template tag", () => {
|
|
11
|
+
it("should parse static classes", () => {
|
|
12
|
+
const result = tw`m-4 p-2 bg-blue-500`;
|
|
13
|
+
expect(result?.style).toEqual({
|
|
14
|
+
margin: 16,
|
|
15
|
+
padding: 8,
|
|
16
|
+
backgroundColor: "#2b7fff",
|
|
17
|
+
});
|
|
18
|
+
expect(result?.activeStyle).toBeUndefined();
|
|
19
|
+
expect(result?.disabledStyle).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should handle interpolated values", () => {
|
|
23
|
+
const isActive = true;
|
|
24
|
+
const result = tw`m-4 ${isActive && "bg-blue-500"}`;
|
|
25
|
+
expect(result?.style).toEqual({
|
|
26
|
+
margin: 16,
|
|
27
|
+
backgroundColor: "#2b7fff",
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should handle conditional classes", () => {
|
|
32
|
+
const isLarge = true;
|
|
33
|
+
const result = tw`p-4 ${isLarge ? "text-xl" : "text-sm"}`;
|
|
34
|
+
expect(result?.style).toEqual({
|
|
35
|
+
padding: 16,
|
|
36
|
+
fontSize: 20,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should handle falsy values", () => {
|
|
41
|
+
const result = tw`m-4 ${false} ${null} ${undefined} p-2`;
|
|
42
|
+
expect(result?.style).toEqual({
|
|
43
|
+
margin: 16,
|
|
44
|
+
padding: 8,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should return empty style object for empty className", () => {
|
|
49
|
+
const result = tw``;
|
|
50
|
+
expect(result).toEqual({ style: {} });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should normalize whitespace", () => {
|
|
54
|
+
const result = tw`m-4 p-2 bg-blue-500`;
|
|
55
|
+
expect(result?.style).toEqual({
|
|
56
|
+
margin: 16,
|
|
57
|
+
padding: 8,
|
|
58
|
+
backgroundColor: "#2b7fff",
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("twStyle function", () => {
|
|
64
|
+
it("should parse className string", () => {
|
|
65
|
+
const result = twStyle("m-4 p-2 bg-blue-500");
|
|
66
|
+
expect(result?.style).toEqual({
|
|
67
|
+
margin: 16,
|
|
68
|
+
padding: 8,
|
|
69
|
+
backgroundColor: "#2b7fff",
|
|
70
|
+
});
|
|
71
|
+
expect(result?.activeStyle).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should return undefined for empty string", () => {
|
|
75
|
+
const result = twStyle("");
|
|
76
|
+
expect(result).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should normalize whitespace", () => {
|
|
80
|
+
const result = twStyle("m-4 p-2 bg-blue-500");
|
|
81
|
+
expect(result?.style).toEqual({
|
|
82
|
+
margin: 16,
|
|
83
|
+
padding: 8,
|
|
84
|
+
backgroundColor: "#2b7fff",
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("setConfig", () => {
|
|
90
|
+
it("should set custom colors", () => {
|
|
91
|
+
setConfig({
|
|
92
|
+
theme: {
|
|
93
|
+
extend: {
|
|
94
|
+
colors: {
|
|
95
|
+
primary: "#007AFF",
|
|
96
|
+
secondary: "#5856D6",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const colors = getCustomColors();
|
|
103
|
+
expect(colors).toEqual({
|
|
104
|
+
primary: "#007AFF",
|
|
105
|
+
secondary: "#5856D6",
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should flatten nested colors", () => {
|
|
110
|
+
setConfig({
|
|
111
|
+
theme: {
|
|
112
|
+
extend: {
|
|
113
|
+
colors: {
|
|
114
|
+
brand: {
|
|
115
|
+
light: "#FF6B6B",
|
|
116
|
+
dark: "#CC0000",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const colors = getCustomColors();
|
|
124
|
+
expect(colors).toEqual({
|
|
125
|
+
"brand-light": "#FF6B6B",
|
|
126
|
+
"brand-dark": "#CC0000",
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("should handle mixed flat and nested colors", () => {
|
|
131
|
+
setConfig({
|
|
132
|
+
theme: {
|
|
133
|
+
extend: {
|
|
134
|
+
colors: {
|
|
135
|
+
primary: "#007AFF",
|
|
136
|
+
brand: {
|
|
137
|
+
light: "#FF6B6B",
|
|
138
|
+
dark: "#CC0000",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const colors = getCustomColors();
|
|
146
|
+
expect(colors).toEqual({
|
|
147
|
+
primary: "#007AFF",
|
|
148
|
+
"brand-light": "#FF6B6B",
|
|
149
|
+
"brand-dark": "#CC0000",
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should clear cache when config changes", () => {
|
|
154
|
+
const style = tw`bg-blue-500`;
|
|
155
|
+
expect(style).toBeDefined();
|
|
156
|
+
expect(getCacheStats().size).toBe(1);
|
|
157
|
+
|
|
158
|
+
setConfig({
|
|
159
|
+
theme: {
|
|
160
|
+
extend: {
|
|
161
|
+
colors: { primary: "#007AFF" },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(getCacheStats().size).toBe(0);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should use custom colors in parsing", () => {
|
|
170
|
+
setConfig({
|
|
171
|
+
theme: {
|
|
172
|
+
extend: {
|
|
173
|
+
colors: {
|
|
174
|
+
primary: "#007AFF",
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const result = tw`bg-primary`;
|
|
181
|
+
expect(result?.style).toEqual({
|
|
182
|
+
backgroundColor: "#007AFF",
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("cache", () => {
|
|
188
|
+
it("should cache parsed styles", () => {
|
|
189
|
+
const result1 = tw`m-4 p-2`;
|
|
190
|
+
const result2 = tw`m-4 p-2`;
|
|
191
|
+
|
|
192
|
+
// Should return the same reference (cached)
|
|
193
|
+
expect(result1).toBe(result2);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should track cache stats", () => {
|
|
197
|
+
const style1 = tw`m-4`;
|
|
198
|
+
const style2 = tw`p-2`;
|
|
199
|
+
const style3 = tw`bg-blue-500`;
|
|
200
|
+
expect(style1).toBeDefined();
|
|
201
|
+
expect(style2).toBeDefined();
|
|
202
|
+
expect(style3).toBeDefined();
|
|
203
|
+
|
|
204
|
+
const stats = getCacheStats();
|
|
205
|
+
expect(stats.size).toBe(3);
|
|
206
|
+
expect(stats.keys).toContain("m-4");
|
|
207
|
+
expect(stats.keys).toContain("p-2");
|
|
208
|
+
expect(stats.keys).toContain("bg-blue-500");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should clear cache", () => {
|
|
212
|
+
const style1 = tw`m-4`;
|
|
213
|
+
const style2 = tw`p-2`;
|
|
214
|
+
expect(style1).toBeDefined();
|
|
215
|
+
expect(style2).toBeDefined();
|
|
216
|
+
expect(getCacheStats().size).toBe(2);
|
|
217
|
+
|
|
218
|
+
clearCache();
|
|
219
|
+
expect(getCacheStats().size).toBe(0);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("state modifiers", () => {
|
|
224
|
+
it("should return activeStyle when active: modifier is used", () => {
|
|
225
|
+
const result = tw`bg-blue-500 active:bg-blue-700`;
|
|
226
|
+
expect(result?.style).toEqual({
|
|
227
|
+
backgroundColor: "#2b7fff",
|
|
228
|
+
});
|
|
229
|
+
expect(result?.activeStyle).toEqual({
|
|
230
|
+
backgroundColor: "#1447e6",
|
|
231
|
+
});
|
|
232
|
+
expect(result?.disabledStyle).toBeUndefined();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should return disabledStyle when disabled: modifier is used", () => {
|
|
236
|
+
const result = tw`bg-blue-500 disabled:bg-gray-300`;
|
|
237
|
+
expect(result?.style).toEqual({
|
|
238
|
+
backgroundColor: "#2b7fff",
|
|
239
|
+
});
|
|
240
|
+
expect(result?.disabledStyle).toEqual({
|
|
241
|
+
backgroundColor: "#d1d5dc",
|
|
242
|
+
});
|
|
243
|
+
expect(result?.activeStyle).toBeUndefined();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("should return both activeStyle and disabledStyle when both modifiers are used", () => {
|
|
247
|
+
const result = tw`bg-blue-500 active:bg-blue-700 disabled:bg-gray-300`;
|
|
248
|
+
expect(result?.style).toEqual({
|
|
249
|
+
backgroundColor: "#2b7fff",
|
|
250
|
+
});
|
|
251
|
+
expect(result?.activeStyle).toEqual({
|
|
252
|
+
backgroundColor: "#1447e6",
|
|
253
|
+
});
|
|
254
|
+
expect(result?.disabledStyle).toEqual({
|
|
255
|
+
backgroundColor: "#d1d5dc",
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("should merge base and active styles with multiple properties", () => {
|
|
260
|
+
const result = tw`p-4 m-2 bg-blue-500 active:bg-blue-700 active:p-6`;
|
|
261
|
+
expect(result?.style).toEqual({
|
|
262
|
+
padding: 16,
|
|
263
|
+
margin: 8,
|
|
264
|
+
backgroundColor: "#2b7fff",
|
|
265
|
+
});
|
|
266
|
+
expect(result?.activeStyle).toEqual({
|
|
267
|
+
backgroundColor: "#1447e6",
|
|
268
|
+
padding: 24,
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("should handle only modifier classes (no base)", () => {
|
|
273
|
+
const result = tw`active:bg-blue-700`;
|
|
274
|
+
expect(result?.style).toEqual({});
|
|
275
|
+
expect(result?.activeStyle).toEqual({
|
|
276
|
+
backgroundColor: "#1447e6",
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("should work with twStyle function", () => {
|
|
281
|
+
const result = twStyle("bg-blue-500 active:bg-blue-700");
|
|
282
|
+
expect(result?.style).toEqual({
|
|
283
|
+
backgroundColor: "#2b7fff",
|
|
284
|
+
});
|
|
285
|
+
expect(result?.activeStyle).toEqual({
|
|
286
|
+
backgroundColor: "#1447e6",
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("should provide raw hex values for animations", () => {
|
|
291
|
+
const result = tw`bg-blue-500 active:bg-blue-700`;
|
|
292
|
+
// Access raw backgroundColor value for use with reanimated
|
|
293
|
+
expect(result?.style.backgroundColor).toBe("#2b7fff");
|
|
294
|
+
expect(result?.activeStyle?.backgroundColor).toBe("#1447e6");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("should return focusStyle when focus: modifier is used", () => {
|
|
298
|
+
const result = tw`bg-blue-500 focus:bg-blue-800`;
|
|
299
|
+
expect(result?.style).toEqual({
|
|
300
|
+
backgroundColor: "#2b7fff",
|
|
301
|
+
});
|
|
302
|
+
expect(result?.focusStyle).toEqual({
|
|
303
|
+
backgroundColor: "#193cb8",
|
|
304
|
+
});
|
|
305
|
+
expect(result?.activeStyle).toBeUndefined();
|
|
306
|
+
expect(result?.disabledStyle).toBeUndefined();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("should return all three modifier styles when all are used", () => {
|
|
310
|
+
const result = tw`bg-blue-500 active:bg-blue-700 focus:bg-blue-800 disabled:bg-gray-300`;
|
|
311
|
+
expect(result?.style).toEqual({
|
|
312
|
+
backgroundColor: "#2b7fff",
|
|
313
|
+
});
|
|
314
|
+
expect(result?.activeStyle).toEqual({
|
|
315
|
+
backgroundColor: "#1447e6",
|
|
316
|
+
});
|
|
317
|
+
expect(result?.focusStyle).toEqual({
|
|
318
|
+
backgroundColor: "#193cb8",
|
|
319
|
+
});
|
|
320
|
+
expect(result?.disabledStyle).toEqual({
|
|
321
|
+
backgroundColor: "#d1d5dc",
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
});
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import type { ImageStyle, TextStyle, ViewStyle } from "react-native";
|
|
2
|
+
import { parseClassName } from "./parser/index.js";
|
|
3
|
+
import { flattenColors } from "./utils/flattenColors.js";
|
|
4
|
+
import { hasModifiers, splitModifierClasses } from "./utils/modifiers.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Union type for all React Native style types
|
|
8
|
+
*/
|
|
9
|
+
export type NativeStyle = ViewStyle | TextStyle | ImageStyle;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Return type for tw/twStyle functions with separate style properties for modifiers
|
|
13
|
+
*/
|
|
14
|
+
export type TwStyle<T extends NativeStyle = NativeStyle> = {
|
|
15
|
+
style: T;
|
|
16
|
+
activeStyle?: T;
|
|
17
|
+
focusStyle?: T;
|
|
18
|
+
disabledStyle?: T;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Runtime configuration type matching Tailwind config structure
|
|
23
|
+
*/
|
|
24
|
+
export type RuntimeConfig = {
|
|
25
|
+
theme?: {
|
|
26
|
+
extend?: {
|
|
27
|
+
colors?: Record<string, string | Record<string, string>>;
|
|
28
|
+
// Future extensions can be added here:
|
|
29
|
+
// spacing?: Record<string, number | string>;
|
|
30
|
+
// fontFamily?: Record<string, string[]>;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Global custom colors configuration
|
|
36
|
+
let globalCustomColors: Record<string, string> | undefined;
|
|
37
|
+
|
|
38
|
+
// Simple memoization cache
|
|
39
|
+
const styleCache = new Map<string, TwStyle>();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Configure runtime Tailwind settings
|
|
43
|
+
* Matches the structure of tailwind.config.mjs for consistency
|
|
44
|
+
*
|
|
45
|
+
* @param config - Runtime configuration object
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* import { setConfig } from '@mgcrea/react-native-tailwind/runtime';
|
|
50
|
+
*
|
|
51
|
+
* setConfig({
|
|
52
|
+
* theme: {
|
|
53
|
+
* extend: {
|
|
54
|
+
* colors: {
|
|
55
|
+
* primary: '#007AFF',
|
|
56
|
+
* secondary: '#5856D6',
|
|
57
|
+
* brand: {
|
|
58
|
+
* light: '#FF6B6B',
|
|
59
|
+
* dark: '#CC0000'
|
|
60
|
+
* }
|
|
61
|
+
* }
|
|
62
|
+
* }
|
|
63
|
+
* }
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function setConfig(config: RuntimeConfig): void {
|
|
68
|
+
// Extract and flatten custom colors
|
|
69
|
+
if (config.theme?.extend?.colors) {
|
|
70
|
+
globalCustomColors = flattenColors(config.theme.extend.colors);
|
|
71
|
+
} else {
|
|
72
|
+
globalCustomColors = undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Clear cache when config changes
|
|
76
|
+
styleCache.clear();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get currently configured custom colors
|
|
81
|
+
*/
|
|
82
|
+
export function getCustomColors(): Record<string, string> | undefined {
|
|
83
|
+
return globalCustomColors;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Clear the memoization cache
|
|
88
|
+
* Useful for testing or when you want to force re-parsing
|
|
89
|
+
*/
|
|
90
|
+
export function clearCache(): void {
|
|
91
|
+
styleCache.clear();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get cache statistics (for debugging/monitoring)
|
|
96
|
+
*/
|
|
97
|
+
export function getCacheStats(): { size: number; keys: string[] } {
|
|
98
|
+
return {
|
|
99
|
+
size: styleCache.size,
|
|
100
|
+
keys: Array.from(styleCache.keys()),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Parse className string and return a TwStyle object with separate modifier properties
|
|
106
|
+
* Internal helper that handles caching and StyleSheet.create wrapping
|
|
107
|
+
*/
|
|
108
|
+
function parseAndCache(className: string): TwStyle {
|
|
109
|
+
// Check cache first
|
|
110
|
+
const cached = styleCache.get(className);
|
|
111
|
+
if (cached) {
|
|
112
|
+
return cached;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check if className contains modifiers
|
|
116
|
+
if (!hasModifiers(className)) {
|
|
117
|
+
// No modifiers - simple case
|
|
118
|
+
const styleObject = parseClassName(className, globalCustomColors);
|
|
119
|
+
|
|
120
|
+
const result: TwStyle = {
|
|
121
|
+
// @ts-expect-error - StyleObject transform types are broader than React Native's strict types
|
|
122
|
+
style: styleObject,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Cache the result
|
|
126
|
+
styleCache.set(className, result);
|
|
127
|
+
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Has modifiers - split and parse separately
|
|
132
|
+
const { base, modifiers } = splitModifierClasses(className);
|
|
133
|
+
|
|
134
|
+
// Parse base styles
|
|
135
|
+
const baseClassName = base.join(" ");
|
|
136
|
+
const baseStyle = baseClassName ? parseClassName(baseClassName, globalCustomColors) : {};
|
|
137
|
+
|
|
138
|
+
// Build result object
|
|
139
|
+
const result: TwStyle = {
|
|
140
|
+
// @ts-expect-error - StyleObject transform types are broader than React Native's strict types
|
|
141
|
+
style: baseStyle,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Parse and add modifier styles
|
|
145
|
+
if (modifiers.has("active")) {
|
|
146
|
+
const activeClasses = modifiers.get("active");
|
|
147
|
+
if (activeClasses && activeClasses.length > 0) {
|
|
148
|
+
const activeClassName = activeClasses.join(" ");
|
|
149
|
+
// @ts-expect-error - StyleObject transform types are broader than React Native's strict types
|
|
150
|
+
result.activeStyle = parseClassName(activeClassName, globalCustomColors);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (modifiers.has("focus")) {
|
|
155
|
+
const focusClasses = modifiers.get("focus");
|
|
156
|
+
if (focusClasses && focusClasses.length > 0) {
|
|
157
|
+
const focusClassName = focusClasses.join(" ");
|
|
158
|
+
// @ts-expect-error - StyleObject transform types are broader than React Native's strict types
|
|
159
|
+
result.focusStyle = parseClassName(focusClassName, globalCustomColors);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (modifiers.has("disabled")) {
|
|
164
|
+
const disabledClasses = modifiers.get("disabled");
|
|
165
|
+
if (disabledClasses && disabledClasses.length > 0) {
|
|
166
|
+
const disabledClassName = disabledClasses.join(" ");
|
|
167
|
+
// @ts-expect-error - StyleObject transform types are broader than React Native's strict types
|
|
168
|
+
result.disabledStyle = parseClassName(disabledClassName, globalCustomColors);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Cache the result
|
|
173
|
+
styleCache.set(className, result);
|
|
174
|
+
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Runtime Tailwind CSS template tag for React Native
|
|
180
|
+
*
|
|
181
|
+
* Parses Tailwind class names at runtime and returns a TwStyle object with separate
|
|
182
|
+
* properties for base styles and modifier styles (active, focus, disabled).
|
|
183
|
+
* Results are memoized for performance.
|
|
184
|
+
*
|
|
185
|
+
* @param strings - Template string parts
|
|
186
|
+
* @param values - Interpolated values
|
|
187
|
+
* @returns TwStyle object with style, activeStyle, focusStyle, and disabledStyle properties
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```tsx
|
|
191
|
+
* import { tw } from '@mgcrea/react-native-tailwind/runtime';
|
|
192
|
+
*
|
|
193
|
+
* // Simple usage - access .style property
|
|
194
|
+
* <View style={tw`m-4 p-2 bg-blue-500`.style} />
|
|
195
|
+
*
|
|
196
|
+
* // With interpolations
|
|
197
|
+
* <View style={tw`flex-1 ${isActive && 'bg-blue-500'} p-4`.style} />
|
|
198
|
+
*
|
|
199
|
+
* // With state modifiers - access activeStyle/focusStyle for animations
|
|
200
|
+
* const styles = tw`bg-blue-500 active:bg-blue-700 focus:bg-blue-800`;
|
|
201
|
+
* <Pressable style={(state) => [
|
|
202
|
+
* styles.style,
|
|
203
|
+
* state.pressed && styles.activeStyle,
|
|
204
|
+
* state.focused && styles.focusStyle
|
|
205
|
+
* ]}>
|
|
206
|
+
* <Text>Press me</Text>
|
|
207
|
+
* </Pressable>
|
|
208
|
+
*
|
|
209
|
+
* // Use with reanimated for animations with raw values
|
|
210
|
+
* const styles = tw`bg-blue-500 active:bg-blue-700`;
|
|
211
|
+
* const animatedStyles = useAnimatedStyle(() => ({
|
|
212
|
+
* ...styles.style,
|
|
213
|
+
* backgroundColor: interpolateColor(
|
|
214
|
+
* progress.value,
|
|
215
|
+
* [0, 1],
|
|
216
|
+
* [styles.style.backgroundColor, styles.activeStyle?.backgroundColor]
|
|
217
|
+
* )
|
|
218
|
+
* }));
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
export function tw<T extends NativeStyle = NativeStyle>(
|
|
222
|
+
strings: TemplateStringsArray,
|
|
223
|
+
...values: unknown[]
|
|
224
|
+
): TwStyle<T> {
|
|
225
|
+
// Combine template strings and values into a single className string
|
|
226
|
+
const className = strings.reduce((acc, str, i) => {
|
|
227
|
+
const value = values[i];
|
|
228
|
+
// Handle falsy values (false, null, undefined) - don't add them
|
|
229
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
230
|
+
const valueStr = value ? String(value) : "";
|
|
231
|
+
return acc + str + valueStr;
|
|
232
|
+
}, "");
|
|
233
|
+
|
|
234
|
+
// Trim and normalize whitespace
|
|
235
|
+
const normalizedClassName = className.trim().replace(/\s+/g, " ");
|
|
236
|
+
|
|
237
|
+
// Handle empty className
|
|
238
|
+
if (!normalizedClassName) {
|
|
239
|
+
return { style: {} as T };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return parseAndCache(normalizedClassName) as TwStyle<T>;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* String version of tw for cases where template literals aren't needed
|
|
247
|
+
*
|
|
248
|
+
* Parses Tailwind class names at runtime and returns a TwStyle object with separate
|
|
249
|
+
* properties for base styles and modifier styles (active, focus, disabled).
|
|
250
|
+
*
|
|
251
|
+
* @param className - Space-separated Tailwind class names
|
|
252
|
+
* @returns TwStyle object with style, activeStyle, focusStyle, and disabledStyle properties
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```tsx
|
|
256
|
+
* import { twStyle } from '@mgcrea/react-native-tailwind/runtime';
|
|
257
|
+
*
|
|
258
|
+
* // Simple usage - access .style property
|
|
259
|
+
* <View style={twStyle('m-4 p-2 bg-blue-500').style} />
|
|
260
|
+
*
|
|
261
|
+
* // With state modifiers
|
|
262
|
+
* const styles = twStyle('bg-blue-500 active:bg-blue-700 focus:bg-blue-800');
|
|
263
|
+
* <Pressable style={(state) => [
|
|
264
|
+
* styles.style,
|
|
265
|
+
* state.pressed && styles.activeStyle,
|
|
266
|
+
* state.focused && styles.focusStyle
|
|
267
|
+
* ]}>
|
|
268
|
+
* <Text>Press me</Text>
|
|
269
|
+
* </Pressable>
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
export function twStyle<T extends NativeStyle = NativeStyle>(className: string): TwStyle<T> | undefined {
|
|
273
|
+
const normalizedClassName = className.trim().replace(/\s+/g, " ");
|
|
274
|
+
|
|
275
|
+
if (!normalizedClassName) {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return parseAndCache(normalizedClassName) as TwStyle<T>;
|
|
280
|
+
}
|