@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.
Files changed (57) hide show
  1. package/README.md +437 -10
  2. package/dist/babel/config-loader.ts +1 -23
  3. package/dist/babel/index.cjs +543 -150
  4. package/dist/babel/index.d.ts +27 -2
  5. package/dist/babel/index.test.ts +268 -0
  6. package/dist/babel/index.ts +352 -44
  7. package/dist/components/Pressable.d.ts +2 -0
  8. package/dist/components/TextInput.d.ts +2 -0
  9. package/dist/config/palettes.d.ts +302 -0
  10. package/dist/config/palettes.js +1 -0
  11. package/dist/index.d.ts +3 -0
  12. package/dist/index.js +1 -1
  13. package/dist/parser/__snapshots__/colors.test.js.snap +242 -90
  14. package/dist/parser/__snapshots__/transforms.test.js.snap +58 -0
  15. package/dist/parser/colors.js +1 -1
  16. package/dist/parser/colors.test.js +1 -1
  17. package/dist/parser/layout.js +1 -1
  18. package/dist/parser/layout.test.js +1 -1
  19. package/dist/parser/typography.js +1 -1
  20. package/dist/parser/typography.test.js +1 -1
  21. package/dist/runtime.cjs +2 -0
  22. package/dist/runtime.cjs.map +7 -0
  23. package/dist/runtime.d.ts +139 -0
  24. package/dist/runtime.js +2 -0
  25. package/dist/runtime.js.map +7 -0
  26. package/dist/runtime.test.js +1 -0
  27. package/dist/stubs/tw.d.ts +60 -0
  28. package/dist/stubs/tw.js +1 -0
  29. package/dist/utils/flattenColors.d.ts +16 -0
  30. package/dist/utils/flattenColors.js +1 -0
  31. package/dist/utils/flattenColors.test.js +1 -0
  32. package/dist/utils/modifiers.d.ts +29 -0
  33. package/dist/utils/modifiers.js +1 -0
  34. package/dist/utils/modifiers.test.js +1 -0
  35. package/dist/utils/styleKey.test.js +1 -0
  36. package/package.json +15 -3
  37. package/src/babel/config-loader.ts +1 -23
  38. package/src/babel/index.test.ts +268 -0
  39. package/src/babel/index.ts +352 -44
  40. package/src/components/Pressable.tsx +1 -0
  41. package/src/components/TextInput.tsx +1 -0
  42. package/src/config/palettes.ts +304 -0
  43. package/src/index.ts +5 -0
  44. package/src/parser/colors.test.ts +47 -31
  45. package/src/parser/colors.ts +5 -110
  46. package/src/parser/layout.test.ts +35 -0
  47. package/src/parser/layout.ts +26 -0
  48. package/src/parser/typography.test.ts +10 -0
  49. package/src/parser/typography.ts +8 -0
  50. package/src/runtime.test.ts +325 -0
  51. package/src/runtime.ts +280 -0
  52. package/src/stubs/tw.ts +80 -0
  53. package/src/utils/flattenColors.test.ts +361 -0
  54. package/src/utils/flattenColors.ts +32 -0
  55. package/src/utils/modifiers.test.ts +286 -0
  56. package/src/utils/modifiers.ts +63 -0
  57. 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
@@ -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
+ }