@mgcrea/react-native-tailwind 0.12.0 → 0.12.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/dist/babel/config-loader.d.ts +3 -0
- package/dist/babel/config-loader.test.ts +2 -2
- package/dist/babel/config-loader.ts +37 -2
- package/dist/babel/index.cjs +136 -22
- package/dist/babel/plugin.ts +11 -1
- package/dist/babel/utils/styleInjection.ts +57 -17
- package/dist/parser/index.d.ts +1 -0
- package/dist/parser/index.js +1 -1
- package/dist/parser/layout.js +1 -1
- package/dist/parser/layout.test.js +1 -1
- package/dist/parser/typography.d.ts +2 -1
- 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 +3 -3
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +3 -3
- package/package.json +1 -1
- package/src/babel/config-loader.test.ts +2 -2
- package/src/babel/config-loader.ts +37 -2
- package/src/babel/plugin.ts +11 -1
- package/src/babel/utils/styleInjection.ts +57 -17
- package/src/parser/index.ts +2 -1
- package/src/parser/layout.test.ts +61 -0
- package/src/parser/layout.ts +55 -1
- package/src/parser/typography.test.ts +102 -0
- package/src/parser/typography.ts +61 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mgcrea/react-native-tailwind",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Compile-time Tailwind CSS for React Native with zero runtime overhead",
|
|
5
5
|
"author": "Olivier Louvignes <olivier@mgcrea.io> (https://github.com/mgcrea)",
|
|
6
6
|
"homepage": "https://github.com/mgcrea/react-native-tailwind#readme",
|
|
@@ -120,7 +120,7 @@ describe("config-loader", () => {
|
|
|
120
120
|
vi.spyOn(fs, "existsSync").mockReturnValue(false);
|
|
121
121
|
|
|
122
122
|
const result = extractCustomTheme("/project/src/file.ts");
|
|
123
|
-
expect(result).toEqual({ colors: {}, fontFamily: {} });
|
|
123
|
+
expect(result).toEqual({ colors: {}, fontFamily: {}, fontSize: {} });
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
it("should return empty theme when config has no theme", () => {
|
|
@@ -134,7 +134,7 @@ describe("config-loader", () => {
|
|
|
134
134
|
const result = extractCustomTheme("/project/src/file.ts");
|
|
135
135
|
|
|
136
136
|
// Without actual config loading, this returns empty
|
|
137
|
-
expect(result).toEqual({ colors: {}, fontFamily: {} });
|
|
137
|
+
expect(result).toEqual({ colors: {}, fontFamily: {}, fontSize: {} });
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
it("should extract colors and fontFamily from theme.extend", () => {
|
|
@@ -14,9 +14,11 @@ export type TailwindConfig = {
|
|
|
14
14
|
extend?: {
|
|
15
15
|
colors?: Record<string, string | Record<string, string>>;
|
|
16
16
|
fontFamily?: Record<string, string | string[]>;
|
|
17
|
+
fontSize?: Record<string, string | number>;
|
|
17
18
|
};
|
|
18
19
|
colors?: Record<string, string | Record<string, string>>;
|
|
19
20
|
fontFamily?: Record<string, string | string[]>;
|
|
21
|
+
fontSize?: Record<string, string | number>;
|
|
20
22
|
};
|
|
21
23
|
};
|
|
22
24
|
|
|
@@ -89,6 +91,7 @@ export function loadTailwindConfig(configPath: string): TailwindConfig | null {
|
|
|
89
91
|
export type CustomTheme = {
|
|
90
92
|
colors: Record<string, string>;
|
|
91
93
|
fontFamily: Record<string, string>;
|
|
94
|
+
fontSize: Record<string, number>;
|
|
92
95
|
};
|
|
93
96
|
|
|
94
97
|
/**
|
|
@@ -100,12 +103,12 @@ export function extractCustomTheme(filename: string): CustomTheme {
|
|
|
100
103
|
const configPath = findTailwindConfig(projectDir);
|
|
101
104
|
|
|
102
105
|
if (!configPath) {
|
|
103
|
-
return { colors: {}, fontFamily: {} };
|
|
106
|
+
return { colors: {}, fontFamily: {}, fontSize: {} };
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
const config = loadTailwindConfig(configPath);
|
|
107
110
|
if (!config?.theme) {
|
|
108
|
-
return { colors: {}, fontFamily: {} };
|
|
111
|
+
return { colors: {}, fontFamily: {}, fontSize: {} };
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
// Extract colors
|
|
@@ -139,8 +142,40 @@ export function extractCustomTheme(filename: string): CustomTheme {
|
|
|
139
142
|
}
|
|
140
143
|
}
|
|
141
144
|
|
|
145
|
+
// Extract fontSize
|
|
146
|
+
/* v8 ignore next 5 */
|
|
147
|
+
if (config.theme.fontSize && !config.theme.extend?.fontSize && process.env.NODE_ENV !== "production") {
|
|
148
|
+
console.warn(
|
|
149
|
+
"[react-native-tailwind] Using theme.fontSize will override all default font sizes. " +
|
|
150
|
+
"Use theme.extend.fontSize to add custom font sizes while keeping defaults.",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
const fontSize = config.theme.extend?.fontSize ?? config.theme.fontSize ?? {};
|
|
154
|
+
|
|
155
|
+
// Convert fontSize values to numbers (handle string or number values)
|
|
156
|
+
const fontSizeResult: Record<string, number> = {};
|
|
157
|
+
for (const [key, value] of Object.entries(fontSize)) {
|
|
158
|
+
if (typeof value === "number") {
|
|
159
|
+
fontSizeResult[key] = value;
|
|
160
|
+
} else if (typeof value === "string") {
|
|
161
|
+
// Parse string values like "18px" or "18" to number
|
|
162
|
+
const parsed = parseFloat(value.replace(/px$/, ""));
|
|
163
|
+
if (!isNaN(parsed)) {
|
|
164
|
+
fontSizeResult[key] = parsed;
|
|
165
|
+
} else {
|
|
166
|
+
/* v8 ignore next 5 */
|
|
167
|
+
if (process.env.NODE_ENV !== "production") {
|
|
168
|
+
console.warn(
|
|
169
|
+
`[react-native-tailwind] Invalid fontSize value for "${key}": ${value}. Expected number or string like "18px".`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
142
176
|
return {
|
|
143
177
|
colors: flattenColors(colors),
|
|
144
178
|
fontFamily: fontFamilyResult,
|
|
179
|
+
fontSize: fontSizeResult,
|
|
145
180
|
};
|
|
146
181
|
}
|
package/src/babel/plugin.ts
CHANGED
|
@@ -398,7 +398,7 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
398
398
|
// Track the local name (could be renamed: import { tw as customTw })
|
|
399
399
|
const localName = spec.local.name;
|
|
400
400
|
state.twImportNames.add(localName);
|
|
401
|
-
|
|
401
|
+
// Don't set hasTwImport yet - only set it when we successfully transform a call
|
|
402
402
|
}
|
|
403
403
|
}
|
|
404
404
|
});
|
|
@@ -443,6 +443,8 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
443
443
|
path.replaceWith(
|
|
444
444
|
t.objectExpression([t.objectProperty(t.identifier("style"), t.objectExpression([]))]),
|
|
445
445
|
);
|
|
446
|
+
// Mark as successfully transformed (even if empty)
|
|
447
|
+
state.hasTwImport = true;
|
|
446
448
|
return;
|
|
447
449
|
}
|
|
448
450
|
|
|
@@ -459,6 +461,9 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
459
461
|
findComponentScope,
|
|
460
462
|
t,
|
|
461
463
|
);
|
|
464
|
+
|
|
465
|
+
// Mark as successfully transformed
|
|
466
|
+
state.hasTwImport = true;
|
|
462
467
|
},
|
|
463
468
|
|
|
464
469
|
// Handle twStyle('...') call expressions
|
|
@@ -502,6 +507,8 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
502
507
|
if (!className) {
|
|
503
508
|
// Replace with undefined
|
|
504
509
|
path.replaceWith(t.identifier("undefined"));
|
|
510
|
+
// Mark as successfully transformed (even if empty)
|
|
511
|
+
state.hasTwImport = true;
|
|
505
512
|
return;
|
|
506
513
|
}
|
|
507
514
|
|
|
@@ -518,6 +525,9 @@ export default function reactNativeTailwindBabelPlugin(
|
|
|
518
525
|
findComponentScope,
|
|
519
526
|
t,
|
|
520
527
|
);
|
|
528
|
+
|
|
529
|
+
// Mark as successfully transformed
|
|
530
|
+
state.hasTwImport = true;
|
|
521
531
|
},
|
|
522
532
|
|
|
523
533
|
JSXAttribute(path, state) {
|
|
@@ -10,24 +10,44 @@ import type { StyleObject } from "../../types/core.js";
|
|
|
10
10
|
* Add StyleSheet import to the file or merge with existing react-native import
|
|
11
11
|
*/
|
|
12
12
|
export function addStyleSheetImport(path: NodePath<BabelTypes.Program>, t: typeof BabelTypes): void {
|
|
13
|
-
// Check if there's already a react-native
|
|
13
|
+
// Check if there's already a value import from react-native
|
|
14
14
|
const body = path.node.body;
|
|
15
|
-
let
|
|
15
|
+
let existingValueImport: BabelTypes.ImportDeclaration | null = null;
|
|
16
16
|
|
|
17
17
|
for (const statement of body) {
|
|
18
18
|
if (t.isImportDeclaration(statement) && statement.source.value === "react-native") {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// Skip type-only imports (they get erased at runtime)
|
|
20
|
+
if (statement.importKind === "type") {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
// Skip namespace imports (import * as RN) - can't add named specifiers to them
|
|
24
|
+
const hasNamespaceImport = statement.specifiers.some((spec) => t.isImportNamespaceSpecifier(spec));
|
|
25
|
+
if (hasNamespaceImport) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
existingValueImport = statement;
|
|
29
|
+
break; // Found a value import, we can stop
|
|
21
30
|
}
|
|
22
31
|
}
|
|
23
32
|
|
|
24
|
-
if (
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
if (existingValueImport) {
|
|
34
|
+
// Check if StyleSheet is already imported
|
|
35
|
+
const hasStyleSheet = existingValueImport.specifiers.some(
|
|
36
|
+
(spec) =>
|
|
37
|
+
t.isImportSpecifier(spec) &&
|
|
38
|
+
spec.imported.type === "Identifier" &&
|
|
39
|
+
spec.imported.name === "StyleSheet",
|
|
28
40
|
);
|
|
41
|
+
|
|
42
|
+
if (!hasStyleSheet) {
|
|
43
|
+
// Add StyleSheet to existing value import
|
|
44
|
+
existingValueImport.specifiers.push(
|
|
45
|
+
t.importSpecifier(t.identifier("StyleSheet"), t.identifier("StyleSheet")),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
29
48
|
} else {
|
|
30
|
-
//
|
|
49
|
+
// No value import exists - create a new one
|
|
50
|
+
// (Don't merge with type-only or namespace imports)
|
|
31
51
|
const importDeclaration = t.importDeclaration(
|
|
32
52
|
[t.importSpecifier(t.identifier("StyleSheet"), t.identifier("StyleSheet"))],
|
|
33
53
|
t.stringLiteral("react-native"),
|
|
@@ -40,22 +60,42 @@ export function addStyleSheetImport(path: NodePath<BabelTypes.Program>, t: typeo
|
|
|
40
60
|
* Add Platform import to the file or merge with existing react-native import
|
|
41
61
|
*/
|
|
42
62
|
export function addPlatformImport(path: NodePath<BabelTypes.Program>, t: typeof BabelTypes): void {
|
|
43
|
-
// Check if there's already a react-native
|
|
63
|
+
// Check if there's already a value import from react-native
|
|
44
64
|
const body = path.node.body;
|
|
45
|
-
let
|
|
65
|
+
let existingValueImport: BabelTypes.ImportDeclaration | null = null;
|
|
46
66
|
|
|
47
67
|
for (const statement of body) {
|
|
48
68
|
if (t.isImportDeclaration(statement) && statement.source.value === "react-native") {
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
// Skip type-only imports (they get erased at runtime)
|
|
70
|
+
if (statement.importKind === "type") {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// Skip namespace imports (import * as RN) - can't add named specifiers to them
|
|
74
|
+
const hasNamespaceImport = statement.specifiers.some((spec) => t.isImportNamespaceSpecifier(spec));
|
|
75
|
+
if (hasNamespaceImport) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
existingValueImport = statement;
|
|
79
|
+
break; // Found a value import, we can stop
|
|
51
80
|
}
|
|
52
81
|
}
|
|
53
82
|
|
|
54
|
-
if (
|
|
55
|
-
//
|
|
56
|
-
|
|
83
|
+
if (existingValueImport) {
|
|
84
|
+
// Check if Platform is already imported
|
|
85
|
+
const hasPlatform = existingValueImport.specifiers.some(
|
|
86
|
+
(spec) =>
|
|
87
|
+
t.isImportSpecifier(spec) && spec.imported.type === "Identifier" && spec.imported.name === "Platform",
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (!hasPlatform) {
|
|
91
|
+
// Add Platform to existing value import
|
|
92
|
+
existingValueImport.specifiers.push(
|
|
93
|
+
t.importSpecifier(t.identifier("Platform"), t.identifier("Platform")),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
57
96
|
} else {
|
|
58
|
-
//
|
|
97
|
+
// No value import exists - create a new one
|
|
98
|
+
// (Don't merge with type-only or namespace imports)
|
|
59
99
|
const importDeclaration = t.importDeclaration(
|
|
60
100
|
[t.importSpecifier(t.identifier("Platform"), t.identifier("Platform"))],
|
|
61
101
|
t.stringLiteral("react-native"),
|
package/src/parser/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { parseTypography } from "./typography";
|
|
|
20
20
|
export type CustomTheme = {
|
|
21
21
|
colors?: Record<string, string>;
|
|
22
22
|
fontFamily?: Record<string, string>;
|
|
23
|
+
fontSize?: Record<string, number>;
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -55,7 +56,7 @@ export function parseClass(cls: string, customTheme?: CustomTheme): StyleObject
|
|
|
55
56
|
parseBorder,
|
|
56
57
|
(cls: string) => parseColor(cls, customTheme?.colors),
|
|
57
58
|
parseLayout,
|
|
58
|
-
(cls: string) => parseTypography(cls, customTheme?.fontFamily),
|
|
59
|
+
(cls: string) => parseTypography(cls, customTheme?.fontFamily, customTheme?.fontSize),
|
|
59
60
|
parseSizing,
|
|
60
61
|
parseShadow,
|
|
61
62
|
parseAspectRatio,
|
|
@@ -73,6 +73,67 @@ describe("parseLayout - flex grow/shrink utilities", () => {
|
|
|
73
73
|
it("should parse shrink-0", () => {
|
|
74
74
|
expect(parseLayout("shrink-0")).toEqual({ flexShrink: 0 });
|
|
75
75
|
});
|
|
76
|
+
|
|
77
|
+
it("should parse grow with arbitrary values", () => {
|
|
78
|
+
expect(parseLayout("grow-[1.5]")).toEqual({ flexGrow: 1.5 });
|
|
79
|
+
expect(parseLayout("grow-[2]")).toEqual({ flexGrow: 2 });
|
|
80
|
+
expect(parseLayout("grow-[0.5]")).toEqual({ flexGrow: 0.5 });
|
|
81
|
+
expect(parseLayout("grow-[3]")).toEqual({ flexGrow: 3 });
|
|
82
|
+
expect(parseLayout("grow-[0]")).toEqual({ flexGrow: 0 });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should parse shrink with arbitrary values", () => {
|
|
86
|
+
expect(parseLayout("shrink-[0.5]")).toEqual({ flexShrink: 0.5 });
|
|
87
|
+
expect(parseLayout("shrink-[2]")).toEqual({ flexShrink: 2 });
|
|
88
|
+
expect(parseLayout("shrink-[1.5]")).toEqual({ flexShrink: 1.5 });
|
|
89
|
+
expect(parseLayout("shrink-[3]")).toEqual({ flexShrink: 3 });
|
|
90
|
+
expect(parseLayout("shrink-[0]")).toEqual({ flexShrink: 0 });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should parse CSS-style flex-grow aliases", () => {
|
|
94
|
+
expect(parseLayout("flex-grow")).toEqual({ flexGrow: 1 });
|
|
95
|
+
expect(parseLayout("flex-grow-0")).toEqual({ flexGrow: 0 });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should parse CSS-style flex-shrink aliases", () => {
|
|
99
|
+
expect(parseLayout("flex-shrink")).toEqual({ flexShrink: 1 });
|
|
100
|
+
expect(parseLayout("flex-shrink-0")).toEqual({ flexShrink: 0 });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should parse CSS-style flex-grow with arbitrary values", () => {
|
|
104
|
+
expect(parseLayout("flex-grow-[1.5]")).toEqual({ flexGrow: 1.5 });
|
|
105
|
+
expect(parseLayout("flex-grow-[2]")).toEqual({ flexGrow: 2 });
|
|
106
|
+
expect(parseLayout("flex-grow-[0.5]")).toEqual({ flexGrow: 0.5 });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should parse CSS-style flex-shrink with arbitrary values", () => {
|
|
110
|
+
expect(parseLayout("flex-shrink-[0.5]")).toEqual({ flexShrink: 0.5 });
|
|
111
|
+
expect(parseLayout("flex-shrink-[2]")).toEqual({ flexShrink: 2 });
|
|
112
|
+
expect(parseLayout("flex-shrink-[1.5]")).toEqual({ flexShrink: 1.5 });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should handle edge case values", () => {
|
|
116
|
+
expect(parseLayout("grow-[0.1]")).toEqual({ flexGrow: 0.1 });
|
|
117
|
+
expect(parseLayout("grow-[10]")).toEqual({ flexGrow: 10 });
|
|
118
|
+
expect(parseLayout("shrink-[0.01]")).toEqual({ flexShrink: 0.01 });
|
|
119
|
+
expect(parseLayout("shrink-[100]")).toEqual({ flexShrink: 100 });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should parse Tailwind shorthand decimals (no leading zero)", () => {
|
|
123
|
+
expect(parseLayout("grow-[.5]")).toEqual({ flexGrow: 0.5 });
|
|
124
|
+
expect(parseLayout("grow-[.75]")).toEqual({ flexGrow: 0.75 });
|
|
125
|
+
expect(parseLayout("shrink-[.5]")).toEqual({ flexShrink: 0.5 });
|
|
126
|
+
expect(parseLayout("shrink-[.25]")).toEqual({ flexShrink: 0.25 });
|
|
127
|
+
expect(parseLayout("flex-grow-[.5]")).toEqual({ flexGrow: 0.5 });
|
|
128
|
+
expect(parseLayout("flex-shrink-[.5]")).toEqual({ flexShrink: 0.5 });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should reject negative values", () => {
|
|
132
|
+
expect(parseLayout("grow-[-1]")).toBeNull();
|
|
133
|
+
expect(parseLayout("shrink-[-1]")).toBeNull();
|
|
134
|
+
expect(parseLayout("flex-grow-[-2]")).toBeNull();
|
|
135
|
+
expect(parseLayout("flex-shrink-[-0.5]")).toBeNull();
|
|
136
|
+
});
|
|
76
137
|
});
|
|
77
138
|
|
|
78
139
|
describe("parseLayout - justify content utilities", () => {
|
package/src/parser/layout.ts
CHANGED
|
@@ -60,6 +60,31 @@ function parseArbitraryZIndex(value: string): number | null {
|
|
|
60
60
|
return null;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Parse arbitrary grow/shrink value: [1.5], [2], [0.5], [.5]
|
|
65
|
+
* Returns number for valid non-negative values, null otherwise
|
|
66
|
+
*/
|
|
67
|
+
function parseArbitraryGrowShrink(value: string): number | null {
|
|
68
|
+
// Match: [1.5], [2], [0], [0.5], [.5] (non-negative decimals, optional leading digit)
|
|
69
|
+
const match = value.match(/^\[(\d+(?:\.\d+)?|\.\d+)\]$/);
|
|
70
|
+
if (match) {
|
|
71
|
+
return parseFloat(match[1]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Warn about invalid formats (negative values, unsupported formats)
|
|
75
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
76
|
+
/* v8 ignore next 5 */
|
|
77
|
+
if (process.env.NODE_ENV !== "production") {
|
|
78
|
+
console.warn(
|
|
79
|
+
`[react-native-tailwind] Invalid arbitrary grow/shrink value: ${value}. Only non-negative numbers are supported (e.g., [1.5], [2], [0.5], [.5]).`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
63
88
|
// Display utilities
|
|
64
89
|
const DISPLAY_MAP: Record<string, StyleObject> = {
|
|
65
90
|
flex: { display: "flex" },
|
|
@@ -88,12 +113,17 @@ const FLEX_MAP: Record<string, StyleObject> = {
|
|
|
88
113
|
"flex-none": { flex: 0 },
|
|
89
114
|
};
|
|
90
115
|
|
|
91
|
-
// Flex grow/shrink utilities
|
|
116
|
+
// Flex grow/shrink utilities (includes CSS-style aliases)
|
|
92
117
|
const GROW_SHRINK_MAP: Record<string, StyleObject> = {
|
|
93
118
|
grow: { flexGrow: 1 },
|
|
94
119
|
"grow-0": { flexGrow: 0 },
|
|
95
120
|
shrink: { flexShrink: 1 },
|
|
96
121
|
"shrink-0": { flexShrink: 0 },
|
|
122
|
+
// CSS-style aliases
|
|
123
|
+
"flex-grow": { flexGrow: 1 },
|
|
124
|
+
"flex-grow-0": { flexGrow: 0 },
|
|
125
|
+
"flex-shrink": { flexShrink: 1 },
|
|
126
|
+
"flex-shrink-0": { flexShrink: 0 },
|
|
97
127
|
};
|
|
98
128
|
|
|
99
129
|
// Justify content utilities
|
|
@@ -357,6 +387,30 @@ export function parseLayout(cls: string): StyleObject | null {
|
|
|
357
387
|
}
|
|
358
388
|
}
|
|
359
389
|
|
|
390
|
+
// Flex grow: grow-[1.5], flex-grow-[2], etc. (arbitrary values)
|
|
391
|
+
if (cls.startsWith("grow-") || cls.startsWith("flex-grow-")) {
|
|
392
|
+
const prefix = cls.startsWith("flex-grow-") ? "flex-grow-" : "grow-";
|
|
393
|
+
const growKey = cls.substring(prefix.length);
|
|
394
|
+
|
|
395
|
+
// Arbitrary values: grow-[1.5], flex-grow-[2]
|
|
396
|
+
const arbitraryGrow = parseArbitraryGrowShrink(growKey);
|
|
397
|
+
if (arbitraryGrow !== null) {
|
|
398
|
+
return { flexGrow: arbitraryGrow };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Flex shrink: shrink-[0.5], flex-shrink-[1], etc. (arbitrary values)
|
|
403
|
+
if (cls.startsWith("shrink-") || cls.startsWith("flex-shrink-")) {
|
|
404
|
+
const prefix = cls.startsWith("flex-shrink-") ? "flex-shrink-" : "shrink-";
|
|
405
|
+
const shrinkKey = cls.substring(prefix.length);
|
|
406
|
+
|
|
407
|
+
// Arbitrary values: shrink-[0.5], flex-shrink-[1]
|
|
408
|
+
const arbitraryShrink = parseArbitraryGrowShrink(shrinkKey);
|
|
409
|
+
if (arbitraryShrink !== null) {
|
|
410
|
+
return { flexShrink: arbitraryShrink };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
360
414
|
// Try each lookup table in order
|
|
361
415
|
return (
|
|
362
416
|
DISPLAY_MAP[cls] ??
|
|
@@ -39,6 +39,20 @@ describe("parseTypography - font size", () => {
|
|
|
39
39
|
expect(parseTypography("text-[22]")).toEqual({ fontSize: 22 });
|
|
40
40
|
expect(parseTypography("text-[100px]")).toEqual({ fontSize: 100 });
|
|
41
41
|
});
|
|
42
|
+
|
|
43
|
+
it("should parse font size with decimal arbitrary values", () => {
|
|
44
|
+
expect(parseTypography("text-[13.5px]")).toEqual({ fontSize: 13.5 });
|
|
45
|
+
expect(parseTypography("text-[13.5]")).toEqual({ fontSize: 13.5 });
|
|
46
|
+
expect(parseTypography("text-[18.75px]")).toEqual({ fontSize: 18.75 });
|
|
47
|
+
expect(parseTypography("text-[18.75]")).toEqual({ fontSize: 18.75 });
|
|
48
|
+
expect(parseTypography("text-[22.5]")).toEqual({ fontSize: 22.5 });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should parse font size with Tailwind shorthand decimals", () => {
|
|
52
|
+
expect(parseTypography("text-[.5]")).toEqual({ fontSize: 0.5 });
|
|
53
|
+
expect(parseTypography("text-[.75px]")).toEqual({ fontSize: 0.75 });
|
|
54
|
+
expect(parseTypography("text-[.5px]")).toEqual({ fontSize: 0.5 });
|
|
55
|
+
});
|
|
42
56
|
});
|
|
43
57
|
|
|
44
58
|
describe("parseTypography - font family", () => {
|
|
@@ -136,6 +150,20 @@ describe("parseTypography - line height", () => {
|
|
|
136
150
|
expect(parseTypography("leading-[30]")).toEqual({ lineHeight: 30 });
|
|
137
151
|
expect(parseTypography("leading-[50px]")).toEqual({ lineHeight: 50 });
|
|
138
152
|
});
|
|
153
|
+
|
|
154
|
+
it("should parse line height with decimal arbitrary values", () => {
|
|
155
|
+
expect(parseTypography("leading-[21.5px]")).toEqual({ lineHeight: 21.5 });
|
|
156
|
+
expect(parseTypography("leading-[21.5]")).toEqual({ lineHeight: 21.5 });
|
|
157
|
+
expect(parseTypography("leading-[28.75px]")).toEqual({ lineHeight: 28.75 });
|
|
158
|
+
expect(parseTypography("leading-[28.75]")).toEqual({ lineHeight: 28.75 });
|
|
159
|
+
expect(parseTypography("leading-[32.5]")).toEqual({ lineHeight: 32.5 });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("should parse line height with Tailwind shorthand decimals", () => {
|
|
163
|
+
expect(parseTypography("leading-[.5]")).toEqual({ lineHeight: 0.5 });
|
|
164
|
+
expect(parseTypography("leading-[.75px]")).toEqual({ lineHeight: 0.75 });
|
|
165
|
+
expect(parseTypography("leading-[.5px]")).toEqual({ lineHeight: 0.5 });
|
|
166
|
+
});
|
|
139
167
|
});
|
|
140
168
|
|
|
141
169
|
describe("parseTypography - letter spacing", () => {
|
|
@@ -149,6 +177,80 @@ describe("parseTypography - letter spacing", () => {
|
|
|
149
177
|
expect(parseTypography("tracking-wider")).toEqual({ letterSpacing: 0.8 });
|
|
150
178
|
expect(parseTypography("tracking-widest")).toEqual({ letterSpacing: 1.6 });
|
|
151
179
|
});
|
|
180
|
+
|
|
181
|
+
it("should parse letter spacing with arbitrary values", () => {
|
|
182
|
+
expect(parseTypography("tracking-[0.5px]")).toEqual({ letterSpacing: 0.5 });
|
|
183
|
+
expect(parseTypography("tracking-[0.5]")).toEqual({ letterSpacing: 0.5 });
|
|
184
|
+
expect(parseTypography("tracking-[0.3]")).toEqual({ letterSpacing: 0.3 });
|
|
185
|
+
expect(parseTypography("tracking-[1.2px]")).toEqual({ letterSpacing: 1.2 });
|
|
186
|
+
expect(parseTypography("tracking-[2]")).toEqual({ letterSpacing: 2 });
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("should parse letter spacing with Tailwind shorthand decimals", () => {
|
|
190
|
+
expect(parseTypography("tracking-[.5]")).toEqual({ letterSpacing: 0.5 });
|
|
191
|
+
expect(parseTypography("tracking-[.3px]")).toEqual({ letterSpacing: 0.3 });
|
|
192
|
+
expect(parseTypography("tracking-[.75]")).toEqual({ letterSpacing: 0.75 });
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("should parse negative letter spacing with arbitrary values", () => {
|
|
196
|
+
expect(parseTypography("tracking-[-0.4px]")).toEqual({ letterSpacing: -0.4 });
|
|
197
|
+
expect(parseTypography("tracking-[-0.4]")).toEqual({ letterSpacing: -0.4 });
|
|
198
|
+
expect(parseTypography("tracking-[-0.5]")).toEqual({ letterSpacing: -0.5 });
|
|
199
|
+
expect(parseTypography("tracking-[-1px]")).toEqual({ letterSpacing: -1 });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("should parse negative letter spacing with shorthand decimals", () => {
|
|
203
|
+
expect(parseTypography("tracking-[-.4]")).toEqual({ letterSpacing: -0.4 });
|
|
204
|
+
expect(parseTypography("tracking-[-.5px]")).toEqual({ letterSpacing: -0.5 });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("should handle edge case letter spacing values", () => {
|
|
208
|
+
expect(parseTypography("tracking-[0]")).toEqual({ letterSpacing: 0 });
|
|
209
|
+
expect(parseTypography("tracking-[0.1]")).toEqual({ letterSpacing: 0.1 });
|
|
210
|
+
expect(parseTypography("tracking-[10]")).toEqual({ letterSpacing: 10 });
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("parseTypography - custom fontSize", () => {
|
|
215
|
+
const customFontSize = {
|
|
216
|
+
tiny: 10,
|
|
217
|
+
huge: 96,
|
|
218
|
+
xl: 22, // Override default (default is 20)
|
|
219
|
+
custom: 17,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
it("should support custom font sizes", () => {
|
|
223
|
+
expect(parseTypography("text-tiny", undefined, customFontSize)).toEqual({ fontSize: 10 });
|
|
224
|
+
expect(parseTypography("text-huge", undefined, customFontSize)).toEqual({ fontSize: 96 });
|
|
225
|
+
expect(parseTypography("text-custom", undefined, customFontSize)).toEqual({ fontSize: 17 });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("should allow custom fontSize to override preset sizes", () => {
|
|
229
|
+
expect(parseTypography("text-xl", undefined, customFontSize)).toEqual({ fontSize: 22 });
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("should fallback to preset sizes when custom fontSize not found", () => {
|
|
233
|
+
expect(parseTypography("text-base", undefined, customFontSize)).toEqual({ fontSize: 16 });
|
|
234
|
+
expect(parseTypography("text-lg", undefined, customFontSize)).toEqual({ fontSize: 18 });
|
|
235
|
+
expect(parseTypography("text-2xl", undefined, customFontSize)).toEqual({ fontSize: 24 });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("should prefer arbitrary values over custom fontSize", () => {
|
|
239
|
+
expect(parseTypography("text-[15px]", undefined, customFontSize)).toEqual({ fontSize: 15 });
|
|
240
|
+
expect(parseTypography("text-[13.5]", undefined, customFontSize)).toEqual({ fontSize: 13.5 });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should work with both customFontFamily and customFontSize", () => {
|
|
244
|
+
const customFontFamily = { brand: "MyCustomFont" };
|
|
245
|
+
expect(parseTypography("text-tiny", customFontFamily, customFontSize)).toEqual({ fontSize: 10 });
|
|
246
|
+
expect(parseTypography("font-brand", customFontFamily, customFontSize)).toEqual({
|
|
247
|
+
fontFamily: "MyCustomFont",
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("should return null for unknown custom fontSize keys", () => {
|
|
252
|
+
expect(parseTypography("text-nonexistent", undefined, customFontSize)).toBeNull();
|
|
253
|
+
});
|
|
152
254
|
});
|
|
153
255
|
|
|
154
256
|
describe("parseTypography - edge cases", () => {
|