@mgcrea/react-native-tailwind 0.11.1 → 0.12.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.
@@ -276,6 +276,10 @@ export default function reactNativeTailwindBabelPlugin(
276
276
  state.twImportNames = new Set();
277
277
  state.hasTwImport = false;
278
278
  state.functionComponentsNeedingColorScheme = new Set();
279
+ state.hasColorSchemeImport = false;
280
+ state.colorSchemeLocalIdentifier = undefined;
281
+ state.needsPlatformImport = false;
282
+ state.hasPlatformImport = false;
279
283
 
280
284
  // Load custom theme from tailwind.config.*
281
285
  state.customTheme = extractCustomTheme(state.file.opts.filename ?? "");
@@ -367,7 +371,8 @@ export default function reactNativeTailwindBabelPlugin(
367
371
 
368
372
  // Track color scheme hook import from the configured source
369
373
  // (default: react-native, but can be custom like @/hooks/useColorScheme)
370
- if (node.source.value === state.colorSchemeImportSource) {
374
+ // Only track value imports (not type-only imports which get erased)
375
+ if (node.source.value === state.colorSchemeImportSource && node.importKind !== "type") {
371
376
  const specifiers = node.specifiers;
372
377
 
373
378
  for (const spec of specifiers) {
@@ -444,7 +449,16 @@ export default function reactNativeTailwindBabelPlugin(
444
449
  state.hasClassNames = true;
445
450
 
446
451
  // Process the className with modifiers
447
- processTwCall(className, path, state, parseClassName, generateStyleKey, splitModifierClasses, t);
452
+ processTwCall(
453
+ className,
454
+ path,
455
+ state,
456
+ parseClassName,
457
+ generateStyleKey,
458
+ splitModifierClasses,
459
+ findComponentScope,
460
+ t,
461
+ );
448
462
  },
449
463
 
450
464
  // Handle twStyle('...') call expressions
@@ -494,7 +508,16 @@ export default function reactNativeTailwindBabelPlugin(
494
508
  state.hasClassNames = true;
495
509
 
496
510
  // Process the className with modifiers
497
- processTwCall(className, path, state, parseClassName, generateStyleKey, splitModifierClasses, t);
511
+ processTwCall(
512
+ className,
513
+ path,
514
+ state,
515
+ parseClassName,
516
+ generateStyleKey,
517
+ splitModifierClasses,
518
+ findComponentScope,
519
+ t,
520
+ );
498
521
  },
499
522
 
500
523
  JSXAttribute(path, state) {
@@ -517,20 +540,22 @@ export default function reactNativeTailwindBabelPlugin(
517
540
  // Determine target style prop based on attribute name
518
541
  const targetStyleProp = getTargetStyleProp(attributeName);
519
542
 
520
- // Handle static string literals
521
- if (t.isStringLiteral(value)) {
522
- const className = value.value.trim();
543
+ /**
544
+ * Process static className string (handles both direct StringLiteral and StringLiteral in JSXExpressionContainer)
545
+ */
546
+ const processStaticClassName = (className: string): boolean => {
547
+ const trimmedClassName = className.trim();
523
548
 
524
549
  // Skip empty classNames
525
- if (!className) {
550
+ if (!trimmedClassName) {
526
551
  path.remove();
527
- return;
552
+ return true;
528
553
  }
529
554
 
530
555
  state.hasClassNames = true;
531
556
 
532
557
  // Check if className contains modifiers (active:, hover:, focus:, placeholder:, ios:, android:, web:, dark:, light:, scheme:)
533
- const { baseClasses, modifierClasses: rawModifierClasses } = splitModifierClasses(className);
558
+ const { baseClasses, modifierClasses: rawModifierClasses } = splitModifierClasses(trimmedClassName);
534
559
 
535
560
  // Expand scheme: modifiers into dark: and light: modifiers
536
561
  const modifierClasses: ParsedModifier[] = [];
@@ -697,7 +722,7 @@ export default function reactNativeTailwindBabelPlugin(
697
722
  } else {
698
723
  replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
699
724
  }
700
- return;
725
+ return true;
701
726
  } else {
702
727
  // Component doesn't support state modifiers, but we can still use platform modifiers
703
728
  // Fall through to platform-only handling
@@ -771,7 +796,7 @@ export default function reactNativeTailwindBabelPlugin(
771
796
  path.node.name = t.jsxIdentifier(targetStyleProp);
772
797
  path.node.value = t.jsxExpressionContainer(styleExpression);
773
798
  }
774
- return;
799
+ return true;
775
800
  }
776
801
 
777
802
  // If there are state modifiers (and no platform modifiers), check if this component supports them
@@ -829,12 +854,12 @@ export default function reactNativeTailwindBabelPlugin(
829
854
  } else {
830
855
  replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
831
856
  }
832
- return;
857
+ return true;
833
858
  }
834
859
  } else {
835
860
  // All modifiers are supported - process normally
836
861
  const styleExpression = processStaticClassNameWithModifiers(
837
- className,
862
+ trimmedClassName,
838
863
  state,
839
864
  parseClassName,
840
865
  generateStyleKey,
@@ -851,7 +876,7 @@ export default function reactNativeTailwindBabelPlugin(
851
876
  } else {
852
877
  replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
853
878
  }
854
- return;
879
+ return true;
855
880
  }
856
881
  } else {
857
882
  // Component doesn't support any modifiers
@@ -871,7 +896,7 @@ export default function reactNativeTailwindBabelPlugin(
871
896
  if (!classNameForStyle) {
872
897
  // No base classes, only had placeholder modifiers - just remove className
873
898
  path.remove();
874
- return;
899
+ return true;
875
900
  }
876
901
 
877
902
  const styleObject = parseClassName(classNameForStyle, state.customTheme);
@@ -888,7 +913,14 @@ export default function reactNativeTailwindBabelPlugin(
888
913
  // Replace className with style prop
889
914
  replaceWithStyleAttribute(path, styleKey, targetStyleProp, state.stylesIdentifier, t);
890
915
  }
891
- return;
916
+ return true;
917
+ };
918
+
919
+ // Handle static string literals
920
+ if (t.isStringLiteral(value)) {
921
+ if (processStaticClassName(value.value)) {
922
+ return;
923
+ }
892
924
  }
893
925
 
894
926
  // Handle dynamic expressions (JSXExpressionContainer)
@@ -900,6 +932,13 @@ export default function reactNativeTailwindBabelPlugin(
900
932
  return;
901
933
  }
902
934
 
935
+ // Fast path: Support string literals wrapped in JSXExpressionContainer: className={"flex-row"}
936
+ if (t.isStringLiteral(expression)) {
937
+ if (processStaticClassName(expression.value)) {
938
+ return;
939
+ }
940
+ }
941
+
903
942
  try {
904
943
  // Find component scope for color scheme modifiers
905
944
  const componentScope = findComponentScope(path, t);
@@ -5,9 +5,16 @@
5
5
  import type { NodePath } from "@babel/core";
6
6
  import type * as BabelTypes from "@babel/types";
7
7
  import type { CustomTheme, ModifierType, ParsedModifier } from "../../parser/index.js";
8
- import { expandSchemeModifier, isSchemeModifier } from "../../parser/index.js";
8
+ import {
9
+ expandSchemeModifier,
10
+ isColorSchemeModifier,
11
+ isPlatformModifier,
12
+ isSchemeModifier,
13
+ } from "../../parser/index.js";
9
14
  import type { SchemeModifierConfig } from "../../types/config.js";
10
15
  import type { StyleObject } from "../../types/core.js";
16
+ import { processColorSchemeModifiers } from "./colorSchemeModifierProcessing.js";
17
+ import { processPlatformModifiers } from "./platformModifierProcessing.js";
11
18
 
12
19
  /**
13
20
  * Plugin state interface (subset needed for tw processing)
@@ -18,11 +25,20 @@ export interface TwProcessingState {
18
25
  customTheme: CustomTheme;
19
26
  schemeModifierConfig: SchemeModifierConfig;
20
27
  stylesIdentifier: string;
28
+ // Color scheme support (for dark:/light: modifiers)
29
+ needsColorSchemeImport: boolean;
30
+ colorSchemeVariableName: string;
31
+ functionComponentsNeedingColorScheme: Set<NodePath<BabelTypes.Function>>;
32
+ colorSchemeLocalIdentifier?: string;
33
+ // Platform support (for ios:/android:/web: modifiers)
34
+ needsPlatformImport: boolean;
21
35
  }
22
36
 
23
37
  /**
24
38
  * Process tw`...` or twStyle('...') call and replace with TwStyle object
25
39
  * Generates: { style: styles._base, activeStyle: styles._active, ... }
40
+ * When color-scheme modifiers are present, generates: { style: [base, _twColorScheme === 'dark' && dark, ...] }
41
+ * When platform modifiers are present, generates: { style: [base, Platform.select({ ios: ..., android: ... })] }
26
42
  */
27
43
  export function processTwCall(
28
44
  className: string,
@@ -31,6 +47,7 @@ export function processTwCall(
31
47
  parseClassName: (className: string, customTheme?: CustomTheme) => StyleObject,
32
48
  generateStyleKey: (className: string) => string,
33
49
  splitModifierClasses: (className: string) => { baseClasses: string[]; modifierClasses: ParsedModifier[] },
50
+ findComponentScope: (path: NodePath, t: typeof BabelTypes) => NodePath<BabelTypes.Function> | null,
34
51
  t: typeof BabelTypes,
35
52
  ): void {
36
53
  const { baseClasses, modifierClasses: rawModifierClasses } = splitModifierClasses(className);
@@ -74,9 +91,200 @@ export function processTwCall(
74
91
  objectProperties.push(t.objectProperty(t.identifier("style"), t.objectExpression([])));
75
92
  }
76
93
 
77
- // Group modifiers by type
94
+ // Separate color-scheme and platform modifiers from other modifiers
95
+ const colorSchemeModifiers = modifierClasses.filter((m) => isColorSchemeModifier(m.modifier));
96
+ const platformModifiers = modifierClasses.filter((m) => isPlatformModifier(m.modifier));
97
+ const otherModifiers = modifierClasses.filter(
98
+ (m) => !isColorSchemeModifier(m.modifier) && !isPlatformModifier(m.modifier),
99
+ );
100
+
101
+ // Check if we need color scheme support
102
+ const hasColorSchemeModifiers = colorSchemeModifiers.length > 0;
103
+ let componentScope: NodePath<BabelTypes.Function> | null = null;
104
+
105
+ if (hasColorSchemeModifiers) {
106
+ // Find component scope for hook injection
107
+ componentScope = findComponentScope(path, t);
108
+
109
+ if (!componentScope) {
110
+ // Warning: color scheme modifiers used outside component scope
111
+ if (process.env.NODE_ENV !== "production") {
112
+ console.warn(
113
+ `[react-native-tailwind] Color scheme modifiers (dark:, light:) in tw/twStyle calls ` +
114
+ `must be used inside a React component. Modifiers will be ignored.`,
115
+ );
116
+ }
117
+ } else {
118
+ // Track this component as needing the color scheme hook
119
+ state.functionComponentsNeedingColorScheme.add(componentScope);
120
+ }
121
+ }
122
+
123
+ // Process color scheme modifiers if we have a valid component scope
124
+ if (hasColorSchemeModifiers && componentScope) {
125
+ // Generate conditional expressions for color scheme
126
+ const colorSchemeConditionals = processColorSchemeModifiers(
127
+ colorSchemeModifiers,
128
+ state,
129
+ parseClassName,
130
+ generateStyleKey,
131
+ t,
132
+ );
133
+
134
+ // Build style array: [baseStyle, _twColorScheme === 'dark' && darkStyle, ...]
135
+ const styleArrayElements: BabelTypes.Expression[] = [];
136
+
137
+ // Add base style if present
138
+ if (baseClasses.length > 0) {
139
+ const baseClassName = baseClasses.join(" ");
140
+ const baseStyleObject = parseClassName(baseClassName, state.customTheme);
141
+ const baseStyleKey = generateStyleKey(baseClassName);
142
+ state.styleRegistry.set(baseStyleKey, baseStyleObject);
143
+ styleArrayElements.push(
144
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey)),
145
+ );
146
+ }
147
+
148
+ // Add color scheme conditionals
149
+ styleArrayElements.push(...colorSchemeConditionals);
150
+
151
+ // Replace style property with array
152
+ objectProperties[0] = t.objectProperty(t.identifier("style"), t.arrayExpression(styleArrayElements));
153
+
154
+ // Also add darkStyle/lightStyle properties for manual processing
155
+ // (e.g., extracting raw hex values for Reanimated animations)
156
+ const darkModifiers = colorSchemeModifiers.filter((m) => m.modifier === "dark");
157
+ const lightModifiers = colorSchemeModifiers.filter((m) => m.modifier === "light");
158
+
159
+ if (darkModifiers.length > 0) {
160
+ const darkClassNames = darkModifiers.map((m) => m.baseClass).join(" ");
161
+ const darkStyleObject = parseClassName(darkClassNames, state.customTheme);
162
+ const darkStyleKey = generateStyleKey(`dark_${darkClassNames}`);
163
+ state.styleRegistry.set(darkStyleKey, darkStyleObject);
164
+
165
+ objectProperties.push(
166
+ t.objectProperty(
167
+ t.identifier("darkStyle"),
168
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(darkStyleKey)),
169
+ ),
170
+ );
171
+ }
172
+
173
+ if (lightModifiers.length > 0) {
174
+ const lightClassNames = lightModifiers.map((m) => m.baseClass).join(" ");
175
+ const lightStyleObject = parseClassName(lightClassNames, state.customTheme);
176
+ const lightStyleKey = generateStyleKey(`light_${lightClassNames}`);
177
+ state.styleRegistry.set(lightStyleKey, lightStyleObject);
178
+
179
+ objectProperties.push(
180
+ t.objectProperty(
181
+ t.identifier("lightStyle"),
182
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(lightStyleKey)),
183
+ ),
184
+ );
185
+ }
186
+ }
187
+
188
+ // Process platform modifiers if present
189
+ const hasPlatformModifiers = platformModifiers.length > 0;
190
+
191
+ if (hasPlatformModifiers) {
192
+ // Mark that we need Platform import
193
+ state.needsPlatformImport = true;
194
+
195
+ // Generate Platform.select() expression
196
+ const platformSelectExpression = processPlatformModifiers(
197
+ platformModifiers,
198
+ state,
199
+ parseClassName,
200
+ generateStyleKey,
201
+ t,
202
+ );
203
+
204
+ // If we already have a style array (from color scheme modifiers), add to it
205
+ // Otherwise, convert style property to an array
206
+ if (hasColorSchemeModifiers && componentScope) {
207
+ // Already have style array from color scheme processing
208
+ // Get the current array expression and add Platform.select to it
209
+ const styleProperty = objectProperties.find(
210
+ (prop) => t.isIdentifier(prop.key) && prop.key.name === "style",
211
+ );
212
+ if (styleProperty && t.isArrayExpression(styleProperty.value)) {
213
+ styleProperty.value.elements.push(platformSelectExpression);
214
+ }
215
+ } else {
216
+ // No color scheme modifiers, create style array with base + Platform.select
217
+ const styleArrayElements: BabelTypes.Expression[] = [];
218
+
219
+ // Add base style if present
220
+ if (baseClasses.length > 0) {
221
+ const baseClassName = baseClasses.join(" ");
222
+ const baseStyleObject = parseClassName(baseClassName, state.customTheme);
223
+ const baseStyleKey = generateStyleKey(baseClassName);
224
+ state.styleRegistry.set(baseStyleKey, baseStyleObject);
225
+ styleArrayElements.push(
226
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey)),
227
+ );
228
+ }
229
+
230
+ // Add Platform.select() expression
231
+ styleArrayElements.push(platformSelectExpression);
232
+
233
+ // Replace style property with array
234
+ objectProperties[0] = t.objectProperty(t.identifier("style"), t.arrayExpression(styleArrayElements));
235
+ }
236
+
237
+ // Also add iosStyle/androidStyle/webStyle properties for manual processing
238
+ const iosModifiers = platformModifiers.filter((m) => m.modifier === "ios");
239
+ const androidModifiers = platformModifiers.filter((m) => m.modifier === "android");
240
+ const webModifiers = platformModifiers.filter((m) => m.modifier === "web");
241
+
242
+ if (iosModifiers.length > 0) {
243
+ const iosClassNames = iosModifiers.map((m) => m.baseClass).join(" ");
244
+ const iosStyleObject = parseClassName(iosClassNames, state.customTheme);
245
+ const iosStyleKey = generateStyleKey(`ios_${iosClassNames}`);
246
+ state.styleRegistry.set(iosStyleKey, iosStyleObject);
247
+
248
+ objectProperties.push(
249
+ t.objectProperty(
250
+ t.identifier("iosStyle"),
251
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(iosStyleKey)),
252
+ ),
253
+ );
254
+ }
255
+
256
+ if (androidModifiers.length > 0) {
257
+ const androidClassNames = androidModifiers.map((m) => m.baseClass).join(" ");
258
+ const androidStyleObject = parseClassName(androidClassNames, state.customTheme);
259
+ const androidStyleKey = generateStyleKey(`android_${androidClassNames}`);
260
+ state.styleRegistry.set(androidStyleKey, androidStyleObject);
261
+
262
+ objectProperties.push(
263
+ t.objectProperty(
264
+ t.identifier("androidStyle"),
265
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(androidStyleKey)),
266
+ ),
267
+ );
268
+ }
269
+
270
+ if (webModifiers.length > 0) {
271
+ const webClassNames = webModifiers.map((m) => m.baseClass).join(" ");
272
+ const webStyleObject = parseClassName(webClassNames, state.customTheme);
273
+ const webStyleKey = generateStyleKey(`web_${webClassNames}`);
274
+ state.styleRegistry.set(webStyleKey, webStyleObject);
275
+
276
+ objectProperties.push(
277
+ t.objectProperty(
278
+ t.identifier("webStyle"),
279
+ t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(webStyleKey)),
280
+ ),
281
+ );
282
+ }
283
+ }
284
+
285
+ // Group other modifiers by type (non-color-scheme and non-platform modifiers)
78
286
  const modifiersByType = new Map<ModifierType, ParsedModifier[]>();
79
- for (const mod of modifierClasses) {
287
+ for (const mod of otherModifiers) {
80
288
  if (!modifiersByType.has(mod.modifier)) {
81
289
  modifiersByType.set(mod.modifier, []);
82
290
  }
@@ -86,7 +294,7 @@ export function processTwCall(
86
294
  }
87
295
  }
88
296
 
89
- // Add modifier styles
297
+ // Add modifier styles (activeStyle, focusStyle, etc.) for non-color-scheme modifiers
90
298
  for (const [modifierType, modifiers] of modifiersByType) {
91
299
  const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
92
300
  const modifierStyleObject = parseClassName(modifierClassNames, state.customTheme);
@@ -238,6 +238,68 @@ describe("parseSpacing - edge cases", () => {
238
238
  });
239
239
  });
240
240
 
241
+ describe("parseSpacing - decimal arbitrary values", () => {
242
+ it("should parse margin with decimal arbitrary values", () => {
243
+ expect(parseSpacing("m-[4.5px]")).toEqual({ margin: 4.5 });
244
+ expect(parseSpacing("m-[4.5]")).toEqual({ margin: 4.5 });
245
+ expect(parseSpacing("m-[16.75px]")).toEqual({ margin: 16.75 });
246
+ expect(parseSpacing("m-[16.75]")).toEqual({ margin: 16.75 });
247
+ expect(parseSpacing("m-[100.25px]")).toEqual({ margin: 100.25 });
248
+ expect(parseSpacing("m-[0.5]")).toEqual({ margin: 0.5 });
249
+ });
250
+
251
+ it("should parse padding with decimal arbitrary values", () => {
252
+ expect(parseSpacing("p-[4.5px]")).toEqual({ padding: 4.5 });
253
+ expect(parseSpacing("p-[4.5]")).toEqual({ padding: 4.5 });
254
+ expect(parseSpacing("pl-[4.5px]")).toEqual({ paddingLeft: 4.5 });
255
+ expect(parseSpacing("pl-[4.5]")).toEqual({ paddingLeft: 4.5 });
256
+ expect(parseSpacing("pr-[16.75px]")).toEqual({ paddingRight: 16.75 });
257
+ expect(parseSpacing("pt-[10.5]")).toEqual({ paddingTop: 10.5 });
258
+ expect(parseSpacing("pb-[20.25px]")).toEqual({ paddingBottom: 20.25 });
259
+ });
260
+
261
+ it("should parse padding horizontal/vertical with decimal arbitrary values", () => {
262
+ expect(parseSpacing("px-[4.5px]")).toEqual({ paddingHorizontal: 4.5 });
263
+ expect(parseSpacing("py-[10.75]")).toEqual({ paddingVertical: 10.75 });
264
+ });
265
+
266
+ it("should parse gap with decimal arbitrary values", () => {
267
+ expect(parseSpacing("gap-[4.5px]")).toEqual({ gap: 4.5 });
268
+ expect(parseSpacing("gap-[4.5]")).toEqual({ gap: 4.5 });
269
+ expect(parseSpacing("gap-[16.75px]")).toEqual({ gap: 16.75 });
270
+ expect(parseSpacing("gap-[0.5]")).toEqual({ gap: 0.5 });
271
+ });
272
+
273
+ it("should parse negative margin with decimal arbitrary values", () => {
274
+ expect(parseSpacing("-m-[4.5px]")).toEqual({ margin: -4.5 });
275
+ expect(parseSpacing("-m-[4.5]")).toEqual({ margin: -4.5 });
276
+ expect(parseSpacing("-m-[10.5px]")).toEqual({ margin: -10.5 });
277
+ expect(parseSpacing("-mt-[16.75px]")).toEqual({ marginTop: -16.75 });
278
+ expect(parseSpacing("-ml-[8.25]")).toEqual({ marginLeft: -8.25 });
279
+ expect(parseSpacing("-mx-[12.5px]")).toEqual({ marginHorizontal: -12.5 });
280
+ expect(parseSpacing("-my-[20.75]")).toEqual({ marginVertical: -20.75 });
281
+ });
282
+
283
+ it("should parse margin directional with decimal arbitrary values", () => {
284
+ expect(parseSpacing("mt-[4.5px]")).toEqual({ marginTop: 4.5 });
285
+ expect(parseSpacing("mr-[8.25]")).toEqual({ marginRight: 8.25 });
286
+ expect(parseSpacing("mb-[16.75px]")).toEqual({ marginBottom: 16.75 });
287
+ expect(parseSpacing("ml-[12.5]")).toEqual({ marginLeft: 12.5 });
288
+ });
289
+
290
+ it("should parse margin horizontal/vertical with decimal arbitrary values", () => {
291
+ expect(parseSpacing("mx-[4.5px]")).toEqual({ marginHorizontal: 4.5 });
292
+ expect(parseSpacing("my-[10.75]")).toEqual({ marginVertical: 10.75 });
293
+ });
294
+
295
+ it("should handle edge case decimal values", () => {
296
+ expect(parseSpacing("m-[0.1px]")).toEqual({ margin: 0.1 });
297
+ expect(parseSpacing("p-[0.001]")).toEqual({ padding: 0.001 });
298
+ expect(parseSpacing("gap-[999.999px]")).toEqual({ gap: 999.999 });
299
+ expect(parseSpacing("-m-[0.5]")).toEqual({ margin: -0.5 });
300
+ });
301
+ });
302
+
241
303
  describe("parseSpacing - comprehensive coverage", () => {
242
304
  it("should parse all margin directions with same value", () => {
243
305
  const value = 16;
@@ -43,14 +43,14 @@ export const SPACING_SCALE: Record<string, number> = {
43
43
  };
44
44
 
45
45
  /**
46
- * Parse arbitrary spacing value: [16px], [20]
47
- * Returns number for px values, null for unsupported formats
46
+ * Parse arbitrary spacing value: [16px], [20], [4.5px], [16.75]
47
+ * Returns number for px values (including decimals), null for unsupported formats
48
48
  */
49
49
  function parseArbitrarySpacing(value: string): number | null {
50
- // Match: [16px] or [16] (pixels only)
51
- const pxMatch = value.match(/^\[(\d+)(?:px)?\]$/);
50
+ // Match: [16px], [16], [4.5px], [4.5] (pixels, including decimals)
51
+ const pxMatch = value.match(/^\[(-?\d+(?:\.\d+)?)(?:px)?\]$/);
52
52
  if (pxMatch) {
53
- return parseInt(pxMatch[1], 10);
53
+ return parseFloat(pxMatch[1]);
54
54
  }
55
55
 
56
56
  // Warn about unsupported formats
@@ -58,7 +58,7 @@ function parseArbitrarySpacing(value: string): number | null {
58
58
  /* v8 ignore next 5 */
59
59
  if (process.env.NODE_ENV !== "production") {
60
60
  console.warn(
61
- `[react-native-tailwind] Unsupported arbitrary spacing value: ${value}. Only px values are supported (e.g., [16px] or [16]).`,
61
+ `[react-native-tailwind] Unsupported arbitrary spacing value: ${value}. Only px values are supported (e.g., [16px], [16], [4.5px], [4.5]).`,
62
62
  );
63
63
  }
64
64
  return null;
@@ -69,7 +69,7 @@ function parseArbitrarySpacing(value: string): number | null {
69
69
 
70
70
  /**
71
71
  * Parse spacing classes (margin, padding, gap)
72
- * Examples: m-4, mx-2, mt-8, p-4, px-2, pt-8, gap-4, m-[16px], -m-4, -mt-[10px]
72
+ * Examples: m-4, mx-2, mt-8, p-4, px-2, pt-8, gap-4, m-[16px], pl-[4.5px], -m-4, -mt-[10px]
73
73
  */
74
74
  export function parseSpacing(cls: string): StyleObject | null {
75
75
  // Margin: m-4, mx-2, mt-8, m-[16px], -m-4, -mt-2, etc.
@@ -317,7 +317,10 @@ describe("runtime", () => {
317
317
  it("should provide raw hex values for animations", () => {
318
318
  const result = tw`bg-blue-500 active:bg-blue-700`;
319
319
  // Access raw backgroundColor value for use with reanimated
320
- expect(result?.style.backgroundColor).toBe("#2b7fff");
320
+ const style = Array.isArray(result?.style) ? result.style.find((s) => s !== false) : result?.style;
321
+ expect(
322
+ style && typeof style === "object" && "backgroundColor" in style ? style.backgroundColor : undefined,
323
+ ).toBe("#2b7fff");
321
324
  expect(result?.activeStyle?.backgroundColor).toBe("#1447e6");
322
325
  });
323
326
 
@@ -7,11 +7,18 @@ export type NativeStyle = ViewStyle | TextStyle | ImageStyle;
7
7
 
8
8
  /**
9
9
  * Return type for tw/twStyle functions with separate style properties for modifiers
10
+ * When color-scheme modifiers (dark:, light:) are present, style becomes an array with runtime conditionals
11
+ * When platform modifiers (ios:, android:, web:) are present, style becomes an array with Platform.select()
10
12
  */
11
13
  export type TwStyle<T extends NativeStyle = NativeStyle> = {
12
- style: T;
14
+ style: T | Array<T | false>;
13
15
  activeStyle?: T;
14
16
  focusStyle?: T;
15
17
  disabledStyle?: T;
16
18
  placeholderStyle?: TextStyle;
19
+ lightStyle?: T;
20
+ darkStyle?: T;
21
+ iosStyle?: T;
22
+ androidStyle?: T;
23
+ webStyle?: T;
17
24
  };