@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.
- package/dist/babel/index.cjs +189 -20
- package/dist/babel/plugin.test.ts +498 -0
- package/dist/babel/plugin.ts +55 -16
- package/dist/babel/utils/twProcessing.d.ts +8 -1
- package/dist/babel/utils/twProcessing.ts +212 -4
- package/dist/parser/spacing.d.ts +1 -1
- package/dist/parser/spacing.js +1 -1
- package/dist/parser/spacing.test.js +1 -1
- package/dist/runtime.cjs +1 -1
- package/dist/runtime.cjs.map +2 -2
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +2 -2
- package/dist/runtime.test.js +1 -1
- package/dist/types/runtime.d.ts +8 -1
- package/package.json +1 -1
- package/src/babel/plugin.test.ts +498 -0
- package/src/babel/plugin.ts +55 -16
- package/src/babel/utils/twProcessing.ts +212 -4
- package/src/parser/spacing.test.ts +62 -0
- package/src/parser/spacing.ts +7 -7
- package/src/runtime.test.ts +4 -1
- package/src/types/runtime.ts +8 -1
package/src/babel/plugin.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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 (!
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
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
|
-
//
|
|
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
|
|
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;
|
package/src/parser/spacing.ts
CHANGED
|
@@ -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]
|
|
51
|
-
const pxMatch = value.match(/^\[(
|
|
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
|
|
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]
|
|
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.
|
package/src/runtime.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/src/types/runtime.ts
CHANGED
|
@@ -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
|
};
|