@mgcrea/react-native-tailwind 0.7.0 → 0.8.1
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 +2 -1
- package/dist/babel/index.cjs +334 -196
- package/dist/babel/index.d.ts +4 -40
- package/dist/babel/index.test.ts +214 -1
- package/dist/babel/index.ts +4 -1169
- package/dist/babel/plugin.d.ts +42 -0
- package/{src/babel/index.test.ts → dist/babel/plugin.test.ts} +216 -2
- package/dist/babel/plugin.ts +491 -0
- package/dist/babel/utils/attributeMatchers.d.ts +23 -0
- package/dist/babel/utils/attributeMatchers.test.ts +294 -0
- package/dist/babel/utils/attributeMatchers.ts +71 -0
- package/dist/babel/utils/componentSupport.d.ts +18 -0
- package/dist/babel/utils/componentSupport.test.ts +426 -0
- package/dist/babel/utils/componentSupport.ts +68 -0
- package/dist/babel/utils/dynamicProcessing.d.ts +32 -0
- package/dist/babel/utils/dynamicProcessing.ts +223 -0
- package/dist/babel/utils/modifierProcessing.d.ts +26 -0
- package/dist/babel/utils/modifierProcessing.ts +118 -0
- package/dist/babel/utils/styleInjection.d.ts +15 -0
- package/dist/babel/utils/styleInjection.ts +80 -0
- package/dist/babel/utils/styleTransforms.d.ts +39 -0
- package/dist/babel/utils/styleTransforms.test.ts +349 -0
- package/dist/babel/utils/styleTransforms.ts +258 -0
- package/dist/babel/utils/twProcessing.d.ts +28 -0
- package/dist/babel/utils/twProcessing.ts +124 -0
- package/dist/components/TextInput.d.ts +171 -14
- package/dist/config/tailwind.d.ts +302 -0
- package/dist/config/tailwind.js +1 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1 -1
- package/dist/parser/colors.js +1 -1
- package/dist/parser/colors.test.js +1 -1
- package/dist/parser/index.d.ts +1 -0
- package/dist/parser/index.js +1 -1
- package/dist/parser/modifiers.d.ts +2 -2
- package/dist/parser/modifiers.js +1 -1
- package/dist/parser/placeholder.d.ts +36 -0
- package/dist/parser/placeholder.js +1 -0
- package/dist/parser/placeholder.test.js +1 -0
- package/dist/parser/typography.d.ts +1 -0
- package/dist/parser/typography.js +1 -1
- package/dist/parser/typography.test.js +1 -1
- package/dist/runtime.cjs +1 -1
- package/dist/runtime.cjs.map +4 -4
- package/dist/runtime.d.ts +1 -14
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +4 -4
- package/dist/stubs/tw.d.ts +1 -14
- package/dist/types/core.d.ts +40 -0
- package/dist/types/core.js +0 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +1 -0
- package/dist/types/runtime.d.ts +15 -0
- package/dist/types/runtime.js +1 -0
- package/dist/types/util.d.ts +3 -0
- package/dist/types/util.js +0 -0
- package/dist/utils/flattenColors.d.ts +1 -0
- package/dist/utils/flattenColors.js +1 -1
- package/dist/utils/flattenColors.test.js +1 -1
- package/package.json +1 -1
- package/src/babel/index.ts +4 -1169
- package/src/babel/plugin.test.ts +482 -0
- package/src/babel/plugin.ts +491 -0
- package/src/babel/utils/attributeMatchers.test.ts +294 -0
- package/src/babel/utils/attributeMatchers.ts +71 -0
- package/src/babel/utils/componentSupport.test.ts +426 -0
- package/src/babel/utils/componentSupport.ts +68 -0
- package/src/babel/utils/dynamicProcessing.ts +223 -0
- package/src/babel/utils/modifierProcessing.ts +118 -0
- package/src/babel/utils/styleInjection.ts +80 -0
- package/src/babel/utils/styleTransforms.test.ts +349 -0
- package/src/babel/utils/styleTransforms.ts +258 -0
- package/src/babel/utils/twProcessing.ts +124 -0
- package/src/components/TextInput.tsx +17 -14
- package/src/config/{palettes.ts → tailwind.ts} +2 -2
- package/src/index.ts +6 -3
- package/src/parser/colors.test.ts +32 -0
- package/src/parser/colors.ts +2 -2
- package/src/parser/index.ts +2 -1
- package/src/parser/modifiers.ts +10 -4
- package/src/parser/placeholder.test.ts +105 -0
- package/src/parser/placeholder.ts +78 -0
- package/src/parser/typography.test.ts +11 -0
- package/src/parser/typography.ts +20 -2
- package/src/runtime.ts +1 -16
- package/src/stubs/tw.ts +1 -16
- package/src/{types.ts → types/core.ts} +0 -4
- package/src/types/index.ts +2 -0
- package/src/types/runtime.ts +17 -0
- package/src/types/util.ts +1 -0
- package/src/utils/flattenColors.test.ts +100 -0
- package/src/utils/flattenColors.ts +3 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for processing tw`...` and twStyle() calls
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { NodePath } from "@babel/core";
|
|
6
|
+
import type * as BabelTypes from "@babel/types";
|
|
7
|
+
import type { ModifierType, ParsedModifier } from "../../parser/index.js";
|
|
8
|
+
import type { StyleObject } from "../../types/core.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Plugin state interface (subset needed for tw processing)
|
|
12
|
+
*/
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
|
14
|
+
export interface TwProcessingState {
|
|
15
|
+
styleRegistry: Map<string, StyleObject>;
|
|
16
|
+
customColors: Record<string, string>;
|
|
17
|
+
stylesIdentifier: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Process tw`...` or twStyle('...') call and replace with TwStyle object
|
|
22
|
+
* Generates: { style: styles._base, activeStyle: styles._active, ... }
|
|
23
|
+
*/
|
|
24
|
+
export function processTwCall(
|
|
25
|
+
className: string,
|
|
26
|
+
path: NodePath,
|
|
27
|
+
state: TwProcessingState,
|
|
28
|
+
parseClassName: (className: string, customColors: Record<string, string>) => StyleObject,
|
|
29
|
+
generateStyleKey: (className: string) => string,
|
|
30
|
+
splitModifierClasses: (className: string) => { baseClasses: string[]; modifierClasses: ParsedModifier[] },
|
|
31
|
+
t: typeof BabelTypes,
|
|
32
|
+
): void {
|
|
33
|
+
const { baseClasses, modifierClasses } = splitModifierClasses(className);
|
|
34
|
+
|
|
35
|
+
// Build TwStyle object properties
|
|
36
|
+
const objectProperties: BabelTypes.ObjectProperty[] = [];
|
|
37
|
+
|
|
38
|
+
// Parse and add base styles
|
|
39
|
+
if (baseClasses.length > 0) {
|
|
40
|
+
const baseClassName = baseClasses.join(" ");
|
|
41
|
+
const baseStyleObject = parseClassName(baseClassName, state.customColors);
|
|
42
|
+
const baseStyleKey = generateStyleKey(baseClassName);
|
|
43
|
+
state.styleRegistry.set(baseStyleKey, baseStyleObject);
|
|
44
|
+
|
|
45
|
+
objectProperties.push(
|
|
46
|
+
t.objectProperty(
|
|
47
|
+
t.identifier("style"),
|
|
48
|
+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(baseStyleKey)),
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
} else {
|
|
52
|
+
// No base classes - add empty style object
|
|
53
|
+
objectProperties.push(t.objectProperty(t.identifier("style"), t.objectExpression([])));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Group modifiers by type
|
|
57
|
+
const modifiersByType = new Map<ModifierType, ParsedModifier[]>();
|
|
58
|
+
for (const mod of modifierClasses) {
|
|
59
|
+
if (!modifiersByType.has(mod.modifier)) {
|
|
60
|
+
modifiersByType.set(mod.modifier, []);
|
|
61
|
+
}
|
|
62
|
+
const modGroup = modifiersByType.get(mod.modifier);
|
|
63
|
+
if (modGroup) {
|
|
64
|
+
modGroup.push(mod);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Add modifier styles
|
|
69
|
+
for (const [modifierType, modifiers] of modifiersByType) {
|
|
70
|
+
const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
|
|
71
|
+
const modifierStyleObject = parseClassName(modifierClassNames, state.customColors);
|
|
72
|
+
const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
|
|
73
|
+
state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
|
|
74
|
+
|
|
75
|
+
// Map modifier type to property name: active -> activeStyle
|
|
76
|
+
const propertyName = `${modifierType}Style`;
|
|
77
|
+
|
|
78
|
+
objectProperties.push(
|
|
79
|
+
t.objectProperty(
|
|
80
|
+
t.identifier(propertyName),
|
|
81
|
+
t.memberExpression(t.identifier(state.stylesIdentifier), t.identifier(modifierStyleKey)),
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Replace the tw`...` or twStyle('...') with the object
|
|
87
|
+
const twStyleObject = t.objectExpression(objectProperties);
|
|
88
|
+
path.replaceWith(twStyleObject);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Remove tw/twStyle imports from @mgcrea/react-native-tailwind
|
|
93
|
+
* This is called after all tw calls have been transformed
|
|
94
|
+
*/
|
|
95
|
+
export function removeTwImports(path: NodePath<BabelTypes.Program>, t: typeof BabelTypes): void {
|
|
96
|
+
// Traverse the program to find and remove tw/twStyle imports
|
|
97
|
+
path.traverse({
|
|
98
|
+
ImportDeclaration(importPath) {
|
|
99
|
+
const node = importPath.node;
|
|
100
|
+
|
|
101
|
+
// Only process imports from main package
|
|
102
|
+
if (node.source.value !== "@mgcrea/react-native-tailwind") {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Filter out tw/twStyle specifiers
|
|
107
|
+
const remainingSpecifiers = node.specifiers.filter((spec) => {
|
|
108
|
+
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported)) {
|
|
109
|
+
const importedName = spec.imported.name;
|
|
110
|
+
return importedName !== "tw" && importedName !== "twStyle";
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (remainingSpecifiers.length === 0) {
|
|
116
|
+
// Remove entire import if no specifiers remain
|
|
117
|
+
importPath.remove();
|
|
118
|
+
} else if (remainingSpecifiers.length < node.specifiers.length) {
|
|
119
|
+
// Update import with remaining specifiers
|
|
120
|
+
node.specifiers = remainingSpecifiers;
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -22,21 +22,24 @@ import {
|
|
|
22
22
|
TextInput as RNTextInput,
|
|
23
23
|
type TextInputProps as RNTextInputProps,
|
|
24
24
|
} from "react-native";
|
|
25
|
+
import { type Simplify } from "../types/util";
|
|
25
26
|
|
|
26
|
-
export type TextInputProps =
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
27
|
+
export type TextInputProps = Simplify<
|
|
28
|
+
Omit<RNTextInputProps, "style"> & {
|
|
29
|
+
/**
|
|
30
|
+
* Style can be a static style object/array or a function that receives focus and disabled state
|
|
31
|
+
*/
|
|
32
|
+
style?:
|
|
33
|
+
| RNTextInputProps["style"]
|
|
34
|
+
| ((state: { focused: boolean; disabled: boolean }) => RNTextInputProps["style"]);
|
|
35
|
+
className?: string; // compile-time only
|
|
36
|
+
/**
|
|
37
|
+
* Convenience prop for disabled state (overrides editable if provided)
|
|
38
|
+
* When true, sets editable to false
|
|
39
|
+
*/
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
}
|
|
42
|
+
>;
|
|
40
43
|
|
|
41
44
|
/**
|
|
42
45
|
* Enhanced TextInput with focus and disabled state support
|
|
@@ -12,7 +12,7 @@ export type TailwindPalette = {
|
|
|
12
12
|
"950": string;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
export const
|
|
15
|
+
export const TAILWIND_COLORS = {
|
|
16
16
|
red: {
|
|
17
17
|
"50": "#fef2f2",
|
|
18
18
|
"100": "#ffe2e2",
|
|
@@ -301,4 +301,4 @@ export const TAILWIND_PALETTES = {
|
|
|
301
301
|
},
|
|
302
302
|
} satisfies Record<string, TailwindPalette>;
|
|
303
303
|
|
|
304
|
-
export type TailwindColor = keyof typeof
|
|
304
|
+
export type TailwindColor = keyof typeof TAILWIND_COLORS;
|
package/src/index.ts
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// Compile-time tw/twStyle functions (transformed by Babel plugin)
|
|
7
|
-
export { tw, twStyle } from "./stubs/tw
|
|
8
|
-
export type { NativeStyle, TwStyle } from "./stubs/tw.js";
|
|
7
|
+
export { tw, twStyle } from "./stubs/tw";
|
|
9
8
|
|
|
10
9
|
// Main parser functions
|
|
11
10
|
export { parseClass, parseClassName } from "./parser";
|
|
@@ -13,7 +12,8 @@ export { flattenColors } from "./utils/flattenColors";
|
|
|
13
12
|
export { generateStyleKey } from "./utils/styleKey";
|
|
14
13
|
|
|
15
14
|
// Re-export types
|
|
16
|
-
export type {
|
|
15
|
+
export type { StyleObject } from "./types/core";
|
|
16
|
+
export type { NativeStyle, TwStyle } from "./types/runtime";
|
|
17
17
|
|
|
18
18
|
// Re-export individual parsers for advanced usage
|
|
19
19
|
export {
|
|
@@ -21,6 +21,8 @@ export {
|
|
|
21
21
|
parseBorder,
|
|
22
22
|
parseColor,
|
|
23
23
|
parseLayout,
|
|
24
|
+
parsePlaceholderClass,
|
|
25
|
+
parsePlaceholderClasses,
|
|
24
26
|
parseShadow,
|
|
25
27
|
parseSizing,
|
|
26
28
|
parseSpacing,
|
|
@@ -41,3 +43,4 @@ export { Pressable } from "./components/Pressable";
|
|
|
41
43
|
export type { PressableProps } from "./components/Pressable";
|
|
42
44
|
export { TextInput } from "./components/TextInput";
|
|
43
45
|
export type { TextInputProps } from "./components/TextInput";
|
|
46
|
+
export { TAILWIND_COLORS } from "./config/tailwind";
|
|
@@ -168,6 +168,38 @@ describe("parseColor - custom colors", () => {
|
|
|
168
168
|
expect(parseColor("bg-blue-500", overrideColors)).toEqual({ backgroundColor: "#FF0000" });
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
+
it("should support custom colors with DEFAULT key from tailwind.config", () => {
|
|
172
|
+
// Simulates what flattenColors() produces from:
|
|
173
|
+
// { primary: { DEFAULT: "#1bacb5", 50: "#eefdfd", ... } }
|
|
174
|
+
const customColorsWithDefault = {
|
|
175
|
+
primary: "#1bacb5", // DEFAULT becomes the parent key
|
|
176
|
+
"primary-50": "#eefdfd",
|
|
177
|
+
"primary-100": "#d4f9f9",
|
|
178
|
+
"primary-500": "#1bacb5",
|
|
179
|
+
"primary-900": "#1e4f5b",
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Test that bg-primary uses the DEFAULT value
|
|
183
|
+
expect(parseColor("bg-primary", customColorsWithDefault)).toEqual({
|
|
184
|
+
backgroundColor: "#1bacb5",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Test that bg-primary-50 uses the shade value
|
|
188
|
+
expect(parseColor("bg-primary-50", customColorsWithDefault)).toEqual({
|
|
189
|
+
backgroundColor: "#eefdfd",
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Test with text colors
|
|
193
|
+
expect(parseColor("text-primary", customColorsWithDefault)).toEqual({
|
|
194
|
+
color: "#1bacb5",
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Test with border colors
|
|
198
|
+
expect(parseColor("border-primary", customColorsWithDefault)).toEqual({
|
|
199
|
+
borderColor: "#1bacb5",
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
171
203
|
it("should fallback to preset colors when custom color not found", () => {
|
|
172
204
|
expect(parseColor("bg-red-500", customColors)).toEqual({ backgroundColor: COLORS["red-500"] });
|
|
173
205
|
});
|
package/src/parser/colors.ts
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* Color utilities (background, text, border colors)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { TAILWIND_COLORS } from "../config/tailwind";
|
|
6
6
|
import type { StyleObject } from "../types";
|
|
7
7
|
import { flattenColors } from "../utils/flattenColors";
|
|
8
8
|
|
|
9
9
|
// Tailwind color palette (flattened from config)
|
|
10
10
|
export const COLORS: Record<string, string> = {
|
|
11
|
-
...flattenColors(
|
|
11
|
+
...flattenColors(TAILWIND_COLORS),
|
|
12
12
|
// Add basic colors
|
|
13
13
|
white: "#FFFFFF",
|
|
14
14
|
black: "#000000",
|
package/src/parser/index.ts
CHANGED
|
@@ -42,7 +42,7 @@ export function parseClass(cls: string, customColors?: Record<string, string>):
|
|
|
42
42
|
// Try each parser in order
|
|
43
43
|
// Note: parseBorder must come before parseColor to avoid border-[3px] being parsed as a color
|
|
44
44
|
// parseColor gets custom colors, others don't need it
|
|
45
|
-
const parsers: (
|
|
45
|
+
const parsers: Array<(cls: string) => StyleObject | null> = [
|
|
46
46
|
parseSpacing,
|
|
47
47
|
parseBorder,
|
|
48
48
|
(cls: string) => parseColor(cls, customColors),
|
|
@@ -74,6 +74,7 @@ export { parseAspectRatio } from "./aspectRatio";
|
|
|
74
74
|
export { parseBorder } from "./borders";
|
|
75
75
|
export { parseColor } from "./colors";
|
|
76
76
|
export { parseLayout } from "./layout";
|
|
77
|
+
export { parsePlaceholderClass, parsePlaceholderClasses } from "./placeholder";
|
|
77
78
|
export { parseShadow } from "./shadows";
|
|
78
79
|
export { parseSizing } from "./sizing";
|
|
79
80
|
export { parseSpacing } from "./spacing";
|
package/src/parser/modifiers.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Modifier parsing utilities for state-based class names (active:, hover:, focus:)
|
|
2
|
+
* Modifier parsing utilities for state-based class names (active:, hover:, focus:, placeholder:)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export type ModifierType = "active" | "hover" | "focus" | "disabled";
|
|
5
|
+
export type ModifierType = "active" | "hover" | "focus" | "disabled" | "placeholder";
|
|
6
6
|
|
|
7
7
|
export type ParsedModifier = {
|
|
8
8
|
modifier: ModifierType;
|
|
@@ -10,9 +10,15 @@ export type ParsedModifier = {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Supported modifiers that map to component states
|
|
13
|
+
* Supported modifiers that map to component states or pseudo-elements
|
|
14
14
|
*/
|
|
15
|
-
const SUPPORTED_MODIFIERS: readonly ModifierType[] = [
|
|
15
|
+
const SUPPORTED_MODIFIERS: readonly ModifierType[] = [
|
|
16
|
+
"active",
|
|
17
|
+
"hover",
|
|
18
|
+
"focus",
|
|
19
|
+
"disabled",
|
|
20
|
+
"placeholder",
|
|
21
|
+
] as const;
|
|
16
22
|
|
|
17
23
|
/**
|
|
18
24
|
* Parse a class name to detect and extract modifiers
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-empty-function */
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { parsePlaceholderClass, parsePlaceholderClasses } from "./placeholder";
|
|
4
|
+
|
|
5
|
+
describe("parsePlaceholderClass", () => {
|
|
6
|
+
it("should parse text color classes", () => {
|
|
7
|
+
// Using actual colors from this project's custom palette (src/config/tailwind.ts)
|
|
8
|
+
expect(parsePlaceholderClass("text-red-500")).toBe("#fb2c36");
|
|
9
|
+
expect(parsePlaceholderClass("text-blue-500")).toBe("#2b7fff");
|
|
10
|
+
expect(parsePlaceholderClass("text-gray-400")).toBe("#99a1af");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should parse text colors with opacity", () => {
|
|
14
|
+
// Using actual colors from custom palette
|
|
15
|
+
// Note: opacity conversion uppercases hex values
|
|
16
|
+
expect(parsePlaceholderClass("text-red-500/50")).toBe("#FB2C3680"); // 50% opacity
|
|
17
|
+
expect(parsePlaceholderClass("text-blue-500/75")).toBe("#2B7FFFBF"); // 75% opacity
|
|
18
|
+
expect(parsePlaceholderClass("text-gray-400/25")).toBe("#99A1AF40"); // 25% opacity
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should parse arbitrary color values", () => {
|
|
22
|
+
expect(parsePlaceholderClass("text-[#ff0000]")).toBe("#ff0000");
|
|
23
|
+
expect(parsePlaceholderClass("text-[#ff0000aa]")).toBe("#ff0000aa");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should parse custom colors", () => {
|
|
27
|
+
const customColors = {
|
|
28
|
+
"brand-primary": "#123456",
|
|
29
|
+
"brand-secondary": "#654321",
|
|
30
|
+
};
|
|
31
|
+
expect(parsePlaceholderClass("text-brand-primary", customColors)).toBe("#123456");
|
|
32
|
+
expect(parsePlaceholderClass("text-brand-secondary", customColors)).toBe("#654321");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should return null for non-text color classes", () => {
|
|
36
|
+
expect(parsePlaceholderClass("font-bold")).toBeNull();
|
|
37
|
+
expect(parsePlaceholderClass("text-lg")).toBeNull();
|
|
38
|
+
expect(parsePlaceholderClass("text-center")).toBeNull();
|
|
39
|
+
expect(parsePlaceholderClass("italic")).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should warn about unsupported utilities in development", () => {
|
|
43
|
+
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
44
|
+
const originalEnv = process.env.NODE_ENV;
|
|
45
|
+
process.env.NODE_ENV = "development";
|
|
46
|
+
|
|
47
|
+
parsePlaceholderClass("font-bold");
|
|
48
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
49
|
+
expect.stringContaining("Only text color utilities are supported in placeholder: modifier"),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
parsePlaceholderClass("text-lg");
|
|
53
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
54
|
+
expect.stringContaining("Only text color utilities are supported in placeholder: modifier"),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
process.env.NODE_ENV = originalEnv;
|
|
58
|
+
consoleSpy.mockRestore();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should not warn in production", () => {
|
|
62
|
+
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
63
|
+
const originalEnv = process.env.NODE_ENV;
|
|
64
|
+
process.env.NODE_ENV = "production";
|
|
65
|
+
|
|
66
|
+
parsePlaceholderClass("font-bold");
|
|
67
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
68
|
+
|
|
69
|
+
process.env.NODE_ENV = originalEnv;
|
|
70
|
+
consoleSpy.mockRestore();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should return null for invalid color values", () => {
|
|
74
|
+
expect(parsePlaceholderClass("text-invalid-color")).toBeNull();
|
|
75
|
+
expect(parsePlaceholderClass("text-[invalid]")).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("parsePlaceholderClasses", () => {
|
|
80
|
+
it("should parse multiple color classes (last wins)", () => {
|
|
81
|
+
expect(parsePlaceholderClasses("text-red-500 text-blue-500")).toBe("#2b7fff"); // blue-500
|
|
82
|
+
expect(parsePlaceholderClasses("text-gray-400 text-green-500")).toBe("#00c950"); // green-500
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should ignore non-color classes and return last valid color", () => {
|
|
86
|
+
expect(parsePlaceholderClasses("text-red-500 font-bold text-blue-500")).toBe("#2b7fff");
|
|
87
|
+
expect(parsePlaceholderClasses("font-bold text-red-500 text-lg")).toBe("#fb2c36");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should return null if no valid colors found", () => {
|
|
91
|
+
expect(parsePlaceholderClasses("font-bold italic text-lg")).toBeNull();
|
|
92
|
+
expect(parsePlaceholderClasses("")).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should handle opacity modifiers", () => {
|
|
96
|
+
// Note: opacity conversion uppercases hex values
|
|
97
|
+
expect(parsePlaceholderClasses("text-red-500/50")).toBe("#FB2C3680");
|
|
98
|
+
expect(parsePlaceholderClasses("text-red-500 text-blue-500/75")).toBe("#2B7FFFBF");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should work with custom colors", () => {
|
|
102
|
+
const customColors = { "brand-primary": "#123456" };
|
|
103
|
+
expect(parsePlaceholderClasses("text-red-500 text-brand-primary", customColors)).toBe("#123456");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placeholder text color utilities (placeholder:text-{color})
|
|
3
|
+
*
|
|
4
|
+
* React Native only supports styling the COLOR of placeholder text via placeholderTextColor prop.
|
|
5
|
+
* Other text styling properties (font-size, font-weight, etc.) are not supported.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { parseColor } from "./colors.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse placeholder modifier classes to extract color
|
|
12
|
+
*
|
|
13
|
+
* Only text-{color} classes are supported inside placeholder: modifier.
|
|
14
|
+
* Other utilities (font-*, text-lg, etc.) will log warnings.
|
|
15
|
+
*
|
|
16
|
+
* @param cls - Class name inside placeholder: modifier (e.g., "text-red-500")
|
|
17
|
+
* @param customColors - Optional custom color palette from tailwind.config
|
|
18
|
+
* @returns Color string (hex with optional alpha) or null if invalid
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* parsePlaceholderClass("text-red-500") // "#ef4444"
|
|
22
|
+
* parsePlaceholderClass("text-red-500/50", {}) // "#ef444480"
|
|
23
|
+
* parsePlaceholderClass("text-brand-primary", { "brand-primary": "#123456" }) // "#123456"
|
|
24
|
+
* parsePlaceholderClass("font-bold") // null (+ warning)
|
|
25
|
+
*/
|
|
26
|
+
export function parsePlaceholderClass(cls: string, customColors?: Record<string, string>): string | null {
|
|
27
|
+
// Check if it's a text color class
|
|
28
|
+
if (!cls.startsWith("text-")) {
|
|
29
|
+
// Warn about unsupported utilities
|
|
30
|
+
if (process.env.NODE_ENV !== "production") {
|
|
31
|
+
console.warn(
|
|
32
|
+
`[react-native-tailwind] Only text color utilities are supported in placeholder: modifier. ` +
|
|
33
|
+
`Class "${cls}" will be ignored. React Native only supports placeholderTextColor prop.`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Parse as color using existing color parser
|
|
40
|
+
// This handles text-red-500, text-red-500/50, text-[#ff0000], etc.
|
|
41
|
+
const styleObject = parseColor(cls, customColors);
|
|
42
|
+
|
|
43
|
+
if (!styleObject?.color) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Return the color value
|
|
48
|
+
return styleObject.color as string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse multiple placeholder classes and return the last valid color
|
|
53
|
+
* (Later classes override earlier ones, matching CSS cascade behavior)
|
|
54
|
+
*
|
|
55
|
+
* @param classes - Space-separated class names
|
|
56
|
+
* @param customColors - Optional custom color palette
|
|
57
|
+
* @returns Color string or null
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* parsePlaceholderClasses("text-red-500 text-blue-500") // "#3b82f6" (blue wins)
|
|
61
|
+
* parsePlaceholderClasses("text-red-500 font-bold") // "#ef4444" (ignores font-bold)
|
|
62
|
+
*/
|
|
63
|
+
export function parsePlaceholderClasses(
|
|
64
|
+
classes: string,
|
|
65
|
+
customColors?: Record<string, string>,
|
|
66
|
+
): string | null {
|
|
67
|
+
const classList = classes.trim().split(/\s+/).filter(Boolean);
|
|
68
|
+
let finalColor: string | null = null;
|
|
69
|
+
|
|
70
|
+
for (const cls of classList) {
|
|
71
|
+
const color = parsePlaceholderClass(cls, customColors);
|
|
72
|
+
if (color) {
|
|
73
|
+
finalColor = color; // Later colors override earlier ones
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return finalColor;
|
|
78
|
+
}
|
|
@@ -118,6 +118,17 @@ describe("parseTypography - line height", () => {
|
|
|
118
118
|
expect(parseTypography("leading-loose")).toEqual({ lineHeight: 32 });
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
it("should parse line height with numeric scale", () => {
|
|
122
|
+
expect(parseTypography("leading-3")).toEqual({ lineHeight: 12 });
|
|
123
|
+
expect(parseTypography("leading-4")).toEqual({ lineHeight: 16 });
|
|
124
|
+
expect(parseTypography("leading-5")).toEqual({ lineHeight: 20 });
|
|
125
|
+
expect(parseTypography("leading-6")).toEqual({ lineHeight: 24 });
|
|
126
|
+
expect(parseTypography("leading-7")).toEqual({ lineHeight: 28 });
|
|
127
|
+
expect(parseTypography("leading-8")).toEqual({ lineHeight: 32 });
|
|
128
|
+
expect(parseTypography("leading-9")).toEqual({ lineHeight: 36 });
|
|
129
|
+
expect(parseTypography("leading-10")).toEqual({ lineHeight: 40 });
|
|
130
|
+
});
|
|
131
|
+
|
|
121
132
|
it("should parse line height with arbitrary pixel values", () => {
|
|
122
133
|
expect(parseTypography("leading-[24px]")).toEqual({ lineHeight: 24 });
|
|
123
134
|
expect(parseTypography("leading-[24]")).toEqual({ lineHeight: 24 });
|
package/src/parser/typography.ts
CHANGED
|
@@ -80,7 +80,19 @@ const TEXT_TRANSFORM_MAP: Record<string, StyleObject> = {
|
|
|
80
80
|
"normal-case": { textTransform: "none" },
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
-
// Line height
|
|
83
|
+
// Line height scale (numeric)
|
|
84
|
+
export const LINE_HEIGHT_SCALE: Record<string, number> = {
|
|
85
|
+
3: 12,
|
|
86
|
+
4: 16,
|
|
87
|
+
5: 20,
|
|
88
|
+
6: 24,
|
|
89
|
+
7: 28,
|
|
90
|
+
8: 32,
|
|
91
|
+
9: 36,
|
|
92
|
+
10: 40,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Line height utilities (named)
|
|
84
96
|
const LINE_HEIGHT_MAP: Record<string, StyleObject> = {
|
|
85
97
|
"leading-none": { lineHeight: 16 },
|
|
86
98
|
"leading-tight": { lineHeight: 20 },
|
|
@@ -169,7 +181,7 @@ export function parseTypography(cls: string): StyleObject | null {
|
|
|
169
181
|
}
|
|
170
182
|
}
|
|
171
183
|
|
|
172
|
-
// Line height: leading-normal, leading-[24px], etc.
|
|
184
|
+
// Line height: leading-normal, leading-6, leading-[24px], etc.
|
|
173
185
|
if (cls.startsWith("leading-")) {
|
|
174
186
|
const heightKey = cls.substring(8);
|
|
175
187
|
|
|
@@ -178,6 +190,12 @@ export function parseTypography(cls: string): StyleObject | null {
|
|
|
178
190
|
if (arbitraryValue !== null) {
|
|
179
191
|
return { lineHeight: arbitraryValue };
|
|
180
192
|
}
|
|
193
|
+
|
|
194
|
+
// Try numeric scale (leading-3, leading-6, etc.)
|
|
195
|
+
const lineHeight = LINE_HEIGHT_SCALE[heightKey];
|
|
196
|
+
if (lineHeight !== undefined) {
|
|
197
|
+
return { lineHeight };
|
|
198
|
+
}
|
|
181
199
|
}
|
|
182
200
|
|
|
183
201
|
// Try each lookup table in order
|
package/src/runtime.ts
CHANGED
|
@@ -1,23 +1,8 @@
|
|
|
1
|
-
import type { ImageStyle, TextStyle, ViewStyle } from "react-native";
|
|
2
1
|
import { parseClassName } from "./parser/index.js";
|
|
2
|
+
import type { NativeStyle, TwStyle } from "./types/runtime.js";
|
|
3
3
|
import { flattenColors } from "./utils/flattenColors.js";
|
|
4
4
|
import { hasModifiers, splitModifierClasses } from "./utils/modifiers.js";
|
|
5
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
6
|
/**
|
|
22
7
|
* Runtime configuration type matching Tailwind config structure
|
|
23
8
|
*/
|
package/src/stubs/tw.ts
CHANGED
|
@@ -7,22 +7,7 @@
|
|
|
7
7
|
* For runtime parsing, use: import { tw } from '@mgcrea/react-native-tailwind/runtime'
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type {
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Union type for all React Native style types
|
|
14
|
-
*/
|
|
15
|
-
export type NativeStyle = ViewStyle | TextStyle | ImageStyle;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Return type for tw/twStyle functions with separate style properties for modifiers
|
|
19
|
-
*/
|
|
20
|
-
export type TwStyle<T extends NativeStyle = NativeStyle> = {
|
|
21
|
-
style: T;
|
|
22
|
-
activeStyle?: T;
|
|
23
|
-
focusStyle?: T;
|
|
24
|
-
disabledStyle?: T;
|
|
25
|
-
};
|
|
10
|
+
import type { NativeStyle, TwStyle } from "../types/runtime.js";
|
|
26
11
|
|
|
27
12
|
/**
|
|
28
13
|
* Compile-time Tailwind CSS template tag (transformed by Babel plugin)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ImageStyle, TextStyle, ViewStyle } from "react-native";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Union type for all React Native style types
|
|
5
|
+
*/
|
|
6
|
+
export type NativeStyle = ViewStyle | TextStyle | ImageStyle;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Return type for tw/twStyle functions with separate style properties for modifiers
|
|
10
|
+
*/
|
|
11
|
+
export type TwStyle<T extends NativeStyle = NativeStyle> = {
|
|
12
|
+
style: T;
|
|
13
|
+
activeStyle?: T;
|
|
14
|
+
focusStyle?: T;
|
|
15
|
+
disabledStyle?: T;
|
|
16
|
+
placeholderStyle?: TextStyle;
|
|
17
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};
|