@mgcrea/react-native-tailwind 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +12 -8
  2. package/dist/babel/config-loader.d.ts +10 -0
  3. package/dist/babel/config-loader.test.ts +75 -21
  4. package/dist/babel/config-loader.ts +100 -2
  5. package/dist/babel/index.cjs +101 -33
  6. package/dist/parser/index.d.ts +1 -0
  7. package/dist/parser/index.js +1 -1
  8. package/dist/parser/layout.d.ts +3 -1
  9. package/dist/parser/layout.js +1 -1
  10. package/dist/parser/layout.test.js +1 -1
  11. package/dist/parser/sizing.d.ts +3 -1
  12. package/dist/parser/sizing.js +1 -1
  13. package/dist/parser/sizing.test.js +1 -1
  14. package/dist/parser/spacing.d.ts +3 -1
  15. package/dist/parser/spacing.js +1 -1
  16. package/dist/parser/spacing.test.js +1 -1
  17. package/dist/parser/transforms.d.ts +3 -1
  18. package/dist/parser/transforms.js +1 -1
  19. package/dist/parser/transforms.test.js +1 -1
  20. package/dist/runtime.cjs +1 -1
  21. package/dist/runtime.cjs.map +3 -3
  22. package/dist/runtime.d.ts +2 -0
  23. package/dist/runtime.js +1 -1
  24. package/dist/runtime.js.map +3 -3
  25. package/dist/runtime.test.js +1 -1
  26. package/package.json +1 -1
  27. package/src/babel/config-loader.test.ts +75 -21
  28. package/src/babel/config-loader.ts +100 -2
  29. package/src/parser/index.ts +6 -5
  30. package/src/parser/layout.test.ts +94 -0
  31. package/src/parser/layout.ts +17 -12
  32. package/src/parser/sizing.test.ts +56 -0
  33. package/src/parser/sizing.ts +20 -15
  34. package/src/parser/spacing.test.ts +57 -0
  35. package/src/parser/spacing.ts +15 -10
  36. package/src/parser/transforms.test.ts +57 -0
  37. package/src/parser/transforms.ts +7 -3
  38. package/src/runtime.test.ts +149 -0
  39. package/src/runtime.ts +53 -1
package/README.md CHANGED
@@ -112,7 +112,7 @@ The Babel plugin transforms your code at compile time:
112
112
  **Input** (what you write):
113
113
 
114
114
  ```tsx
115
- <View className="m-4 p-2 bg-blue-500 rounded-lg" />
115
+ <View className={`rounded-lg p-4 ${isSelected ? "bg-blue-500 border border-blue-700" : "bg-gray-200"}`} />
116
116
  ```
117
117
 
118
118
  **Output** (what Babel generates):
@@ -120,15 +120,19 @@ The Babel plugin transforms your code at compile time:
120
120
  ```tsx
121
121
  import { StyleSheet } from "react-native";
122
122
 
123
- <View style={_twStyles._bg_blue_500_m_4_p_2_rounded_lg} />;
123
+ <View
124
+ style={[
125
+ _twStyles._rounded_lg,
126
+ _twStyles._p_4,
127
+ isSelected ? _twStyles._bg_blue_500_border_border_blue_700 : _twStyles._bg_gray_200,
128
+ ]}
129
+ />;
124
130
 
125
131
  const _twStyles = StyleSheet.create({
126
- _bg_blue_500_m_4_p_2_rounded_lg: {
127
- margin: 16,
128
- padding: 8,
129
- backgroundColor: "#3B82F6",
130
- borderRadius: 8,
131
- },
132
+ _rounded_lg: { borderRadius: 8 },
133
+ _p_4: { padding: 16 },
134
+ _bg_blue_500_border_border_blue_700: { backgroundColor: "#3B82F6", borderWidth: 1, borderColor: "#1D4ED8" },
135
+ _bg_gray_200: { backgroundColor: "#E5E7EB" },
132
136
  });
133
137
  ```
134
138
 
@@ -8,12 +8,21 @@ export type TailwindConfig = {
8
8
  colors?: Record<string, string | Record<string, string>>;
9
9
  fontFamily?: Record<string, string | string[]>;
10
10
  fontSize?: Record<string, string | number>;
11
+ spacing?: Record<string, string | number>;
12
+ [key: string]: unknown;
11
13
  };
12
14
  colors?: Record<string, string | Record<string, string>>;
13
15
  fontFamily?: Record<string, string | string[]>;
14
16
  fontSize?: Record<string, string | number>;
17
+ spacing?: Record<string, string | number>;
18
+ [key: string]: unknown;
15
19
  };
16
20
  };
21
+ /**
22
+ * Check for unsupported theme extensions and warn the user
23
+ * @internal Exported for testing
24
+ */
25
+ export declare function warnUnsupportedThemeKeys(config: TailwindConfig, configPath: string): void;
17
26
  /**
18
27
  * Find tailwind.config.* file by traversing up from startDir
19
28
  */
@@ -29,6 +38,7 @@ export type CustomTheme = {
29
38
  colors: Record<string, string>;
30
39
  fontFamily: Record<string, string>;
31
40
  fontSize: Record<string, number>;
41
+ spacing: Record<string, number>;
32
42
  };
33
43
  /**
34
44
  * Extract all custom theme extensions from tailwind config
@@ -1,6 +1,11 @@
1
1
  import * as fs from "fs";
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
- import { extractCustomTheme, findTailwindConfig, loadTailwindConfig } from "./config-loader";
3
+ import {
4
+ extractCustomTheme,
5
+ findTailwindConfig,
6
+ loadTailwindConfig,
7
+ warnUnsupportedThemeKeys,
8
+ } from "./config-loader";
4
9
 
5
10
  // Mock fs
6
11
  vi.mock("fs");
@@ -120,35 +125,84 @@ describe("config-loader", () => {
120
125
  vi.spyOn(fs, "existsSync").mockReturnValue(false);
121
126
 
122
127
  const result = extractCustomTheme("/project/src/file.ts");
123
- expect(result).toEqual({ colors: {}, fontFamily: {}, fontSize: {} });
128
+ expect(result).toEqual({ colors: {}, fontFamily: {}, fontSize: {}, spacing: {} });
124
129
  });
130
+ });
125
131
 
126
- it("should return empty theme when config has no theme", () => {
127
- const configPath = "/project/tailwind.config.js";
132
+ describe("warnUnsupportedThemeKeys", () => {
133
+ it("should warn about unsupported theme keys", () => {
134
+ const configPath = "/project/unsupported/tailwind.config.js";
135
+ const mockConfig = {
136
+ theme: {
137
+ extend: {
138
+ colors: { brand: "#123456" },
139
+ spacing: { "72": "18rem" }, // Supported (now!)
140
+ borderRadius: { xl: "1rem" }, // Unsupported
141
+ lineHeight: { tight: "1.25" }, // Unsupported
142
+ },
143
+ screens: { tablet: "640px" }, // Unsupported
144
+ },
145
+ };
128
146
 
129
- vi.spyOn(fs, "existsSync").mockImplementation((filepath) => filepath === configPath);
130
- vi.spyOn(require, "resolve").mockReturnValue(configPath);
147
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation(vi.fn());
131
148
 
132
- // loadTailwindConfig will be called, but we've already tested it
133
- // For integration, we'd need to mock the entire flow
134
- const result = extractCustomTheme("/project/src/file.ts");
149
+ warnUnsupportedThemeKeys(mockConfig, configPath);
135
150
 
136
- // Without actual config loading, this returns empty
137
- expect(result).toEqual({ colors: {}, fontFamily: {}, fontSize: {} });
151
+ expect(consoleSpy).toHaveBeenCalledWith(
152
+ expect.stringContaining("Unsupported theme configuration detected"),
153
+ );
154
+ // spacing is now supported, so should NOT warn about it
155
+ expect(consoleSpy).not.toHaveBeenCalledWith(expect.stringContaining("theme.extend.spacing"));
156
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("theme.extend.borderRadius"));
157
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("theme.extend.lineHeight"));
158
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("theme.screens"));
159
+ expect(consoleSpy).toHaveBeenCalledWith(
160
+ expect.stringContaining("https://github.com/mgcrea/react-native-tailwind/issues/new"),
161
+ );
162
+
163
+ consoleSpy.mockRestore();
138
164
  });
139
165
 
140
- it("should extract colors and fontFamily from theme.extend", () => {
141
- // This would require complex mocking of the entire require flow
142
- // Testing the logic: theme.extend is preferred
143
- const colors = { brand: { light: "#fff", dark: "#000" } };
144
- const fontFamily = { sans: ['"SF Pro"'], custom: ['"Custom Font"'] };
145
- const theme = {
146
- extend: { colors, fontFamily },
166
+ it("should not warn for supported theme keys only", () => {
167
+ const configPath = "/project/supported/tailwind.config.js";
168
+ const mockConfig = {
169
+ theme: {
170
+ extend: {
171
+ colors: { brand: "#123456" },
172
+ fontFamily: { custom: "CustomFont" },
173
+ fontSize: { huge: "48px" },
174
+ spacing: { "72": "18rem" },
175
+ },
176
+ },
177
+ };
178
+
179
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation(vi.fn());
180
+
181
+ warnUnsupportedThemeKeys(mockConfig, configPath);
182
+
183
+ expect(consoleSpy).not.toHaveBeenCalled();
184
+
185
+ consoleSpy.mockRestore();
186
+ });
187
+
188
+ it("should only warn once per config path", () => {
189
+ const configPath = "/project/once/tailwind.config.js";
190
+ const mockConfig = {
191
+ theme: {
192
+ extend: {
193
+ borderRadius: { xl: "1rem" }, // Unsupported
194
+ },
195
+ },
147
196
  };
148
197
 
149
- // If we had the config, we'd flatten the colors and convert fontFamily
150
- expect(theme.extend.colors).toEqual(colors);
151
- expect(theme.extend.fontFamily).toEqual(fontFamily);
198
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation(vi.fn());
199
+
200
+ warnUnsupportedThemeKeys(mockConfig, configPath);
201
+ warnUnsupportedThemeKeys(mockConfig, configPath);
202
+
203
+ expect(consoleSpy).toHaveBeenCalledTimes(1);
204
+
205
+ consoleSpy.mockRestore();
152
206
  });
153
207
  });
154
208
  });
@@ -15,13 +15,68 @@ export type TailwindConfig = {
15
15
  colors?: Record<string, string | Record<string, string>>;
16
16
  fontFamily?: Record<string, string | string[]>;
17
17
  fontSize?: Record<string, string | number>;
18
+ spacing?: Record<string, string | number>;
19
+ [key: string]: unknown;
18
20
  };
19
21
  colors?: Record<string, string | Record<string, string>>;
20
22
  fontFamily?: Record<string, string | string[]>;
21
23
  fontSize?: Record<string, string | number>;
24
+ spacing?: Record<string, string | number>;
25
+ [key: string]: unknown;
22
26
  };
23
27
  };
24
28
 
29
+ /**
30
+ * Theme keys currently supported by react-native-tailwind
31
+ */
32
+ const SUPPORTED_THEME_KEYS = new Set(["colors", "fontFamily", "fontSize", "spacing", "extend"]);
33
+
34
+ /**
35
+ * Cache for warned config paths to avoid duplicate warnings
36
+ */
37
+ const warnedConfigPaths = new Set<string>();
38
+
39
+ /**
40
+ * Check for unsupported theme extensions and warn the user
41
+ * @internal Exported for testing
42
+ */
43
+ export function warnUnsupportedThemeKeys(config: TailwindConfig, configPath: string): void {
44
+ if (process.env.NODE_ENV === "production" || warnedConfigPaths.has(configPath)) {
45
+ return;
46
+ }
47
+
48
+ const unsupportedKeys: string[] = [];
49
+
50
+ // Check theme.extend keys
51
+ if (config.theme?.extend && typeof config.theme.extend === "object") {
52
+ for (const key of Object.keys(config.theme.extend)) {
53
+ if (!SUPPORTED_THEME_KEYS.has(key)) {
54
+ unsupportedKeys.push(`theme.extend.${key}`);
55
+ }
56
+ }
57
+ }
58
+
59
+ // Check direct theme keys (excluding 'extend')
60
+ if (config.theme && typeof config.theme === "object") {
61
+ for (const key of Object.keys(config.theme)) {
62
+ if (key !== "extend" && !SUPPORTED_THEME_KEYS.has(key)) {
63
+ unsupportedKeys.push(`theme.${key}`);
64
+ }
65
+ }
66
+ }
67
+
68
+ if (unsupportedKeys.length > 0) {
69
+ warnedConfigPaths.add(configPath);
70
+ console.warn(
71
+ `[react-native-tailwind] Unsupported theme configuration detected:\n` +
72
+ ` ${unsupportedKeys.join(", ")}\n\n` +
73
+ ` Currently supported: colors, fontFamily, fontSize, spacing\n\n` +
74
+ ` These extensions will be ignored. If you need support for these features,\n` +
75
+ ` please open an issue: https://github.com/mgcrea/react-native-tailwind/issues/new`,
76
+ );
77
+ }
78
+ }
79
+
25
80
  // Cache configs per path to avoid repeated file I/O
26
81
  const configCache = new Map<string, TailwindConfig | null>();
27
82
 
@@ -92,6 +147,7 @@ export type CustomTheme = {
92
147
  colors: Record<string, string>;
93
148
  fontFamily: Record<string, string>;
94
149
  fontSize: Record<string, number>;
150
+ spacing: Record<string, number>;
95
151
  };
96
152
 
97
153
  /**
@@ -103,14 +159,17 @@ export function extractCustomTheme(filename: string): CustomTheme {
103
159
  const configPath = findTailwindConfig(projectDir);
104
160
 
105
161
  if (!configPath) {
106
- return { colors: {}, fontFamily: {}, fontSize: {} };
162
+ return { colors: {}, fontFamily: {}, fontSize: {}, spacing: {} };
107
163
  }
108
164
 
109
165
  const config = loadTailwindConfig(configPath);
110
166
  if (!config?.theme) {
111
- return { colors: {}, fontFamily: {}, fontSize: {} };
167
+ return { colors: {}, fontFamily: {}, fontSize: {}, spacing: {} };
112
168
  }
113
169
 
170
+ // Warn about unsupported theme keys
171
+ warnUnsupportedThemeKeys(config, configPath);
172
+
114
173
  // Extract colors
115
174
  /* v8 ignore next 5 */
116
175
  if (config.theme.colors && !config.theme.extend?.colors && process.env.NODE_ENV !== "production") {
@@ -173,9 +232,48 @@ export function extractCustomTheme(filename: string): CustomTheme {
173
232
  }
174
233
  }
175
234
 
235
+ // Extract spacing
236
+ /* v8 ignore next 5 */
237
+ if (config.theme.spacing && !config.theme.extend?.spacing && process.env.NODE_ENV !== "production") {
238
+ console.warn(
239
+ "[react-native-tailwind] Using theme.spacing will override all default spacing. " +
240
+ "Use theme.extend.spacing to add custom spacing while keeping defaults.",
241
+ );
242
+ }
243
+ const spacing = config.theme.extend?.spacing ?? config.theme.spacing ?? {};
244
+
245
+ // Convert spacing values to numbers (handle rem, px, or number values)
246
+ const spacingResult: Record<string, number> = {};
247
+ for (const [key, value] of Object.entries(spacing)) {
248
+ if (typeof value === "number") {
249
+ spacingResult[key] = value;
250
+ } else if (typeof value === "string") {
251
+ // Parse string values: "18rem" -> 288, "16px" -> 16, "16" -> 16
252
+ let parsed: number;
253
+ if (value.endsWith("rem")) {
254
+ // Convert rem to px (1rem = 16px)
255
+ parsed = parseFloat(value.replace(/rem$/, "")) * 16;
256
+ } else {
257
+ // Parse px or unitless values
258
+ parsed = parseFloat(value.replace(/px$/, ""));
259
+ }
260
+ if (!isNaN(parsed)) {
261
+ spacingResult[key] = parsed;
262
+ } else {
263
+ /* v8 ignore next 5 */
264
+ if (process.env.NODE_ENV !== "production") {
265
+ console.warn(
266
+ `[react-native-tailwind] Invalid spacing value for "${key}": ${value}. Expected number or string like "16px" or "1rem".`,
267
+ );
268
+ }
269
+ }
270
+ }
271
+ }
272
+
176
273
  return {
177
274
  colors: flattenColors(colors),
178
275
  fontFamily: fontFamilyResult,
179
276
  fontSize: fontSizeResult,
277
+ spacing: spacingResult,
180
278
  };
181
279
  }
@@ -53,6 +53,40 @@ function flattenColors(colors, prefix = "") {
53
53
  }
54
54
 
55
55
  // src/babel/config-loader.ts
56
+ var SUPPORTED_THEME_KEYS = /* @__PURE__ */ new Set(["colors", "fontFamily", "fontSize", "spacing", "extend"]);
57
+ var warnedConfigPaths = /* @__PURE__ */ new Set();
58
+ function warnUnsupportedThemeKeys(config, configPath) {
59
+ if (process.env.NODE_ENV === "production" || warnedConfigPaths.has(configPath)) {
60
+ return;
61
+ }
62
+ const unsupportedKeys = [];
63
+ if (config.theme?.extend && typeof config.theme.extend === "object") {
64
+ for (const key of Object.keys(config.theme.extend)) {
65
+ if (!SUPPORTED_THEME_KEYS.has(key)) {
66
+ unsupportedKeys.push(`theme.extend.${key}`);
67
+ }
68
+ }
69
+ }
70
+ if (config.theme && typeof config.theme === "object") {
71
+ for (const key of Object.keys(config.theme)) {
72
+ if (key !== "extend" && !SUPPORTED_THEME_KEYS.has(key)) {
73
+ unsupportedKeys.push(`theme.${key}`);
74
+ }
75
+ }
76
+ }
77
+ if (unsupportedKeys.length > 0) {
78
+ warnedConfigPaths.add(configPath);
79
+ console.warn(
80
+ `[react-native-tailwind] Unsupported theme configuration detected:
81
+ ${unsupportedKeys.join(", ")}
82
+
83
+ Currently supported: colors, fontFamily, fontSize, spacing
84
+
85
+ These extensions will be ignored. If you need support for these features,
86
+ please open an issue: https://github.com/mgcrea/react-native-tailwind/issues/new`
87
+ );
88
+ }
89
+ }
56
90
  var configCache = /* @__PURE__ */ new Map();
57
91
  function findTailwindConfig(startDir) {
58
92
  let currentDir = startDir;
@@ -97,12 +131,13 @@ function extractCustomTheme(filename) {
97
131
  const projectDir = path.dirname(filename);
98
132
  const configPath = findTailwindConfig(projectDir);
99
133
  if (!configPath) {
100
- return { colors: {}, fontFamily: {}, fontSize: {} };
134
+ return { colors: {}, fontFamily: {}, fontSize: {}, spacing: {} };
101
135
  }
102
136
  const config = loadTailwindConfig(configPath);
103
137
  if (!config?.theme) {
104
- return { colors: {}, fontFamily: {}, fontSize: {} };
138
+ return { colors: {}, fontFamily: {}, fontSize: {}, spacing: {} };
105
139
  }
140
+ warnUnsupportedThemeKeys(config, configPath);
106
141
  if (config.theme.colors && !config.theme.extend?.colors && process.env.NODE_ENV !== "production") {
107
142
  console.warn(
108
143
  "[react-native-tailwind] Using theme.colors will override all default colors. Use theme.extend.colors to add custom colors while keeping defaults."
@@ -146,10 +181,39 @@ function extractCustomTheme(filename) {
146
181
  }
147
182
  }
148
183
  }
184
+ if (config.theme.spacing && !config.theme.extend?.spacing && process.env.NODE_ENV !== "production") {
185
+ console.warn(
186
+ "[react-native-tailwind] Using theme.spacing will override all default spacing. Use theme.extend.spacing to add custom spacing while keeping defaults."
187
+ );
188
+ }
189
+ const spacing = config.theme.extend?.spacing ?? config.theme.spacing ?? {};
190
+ const spacingResult = {};
191
+ for (const [key, value] of Object.entries(spacing)) {
192
+ if (typeof value === "number") {
193
+ spacingResult[key] = value;
194
+ } else if (typeof value === "string") {
195
+ let parsed;
196
+ if (value.endsWith("rem")) {
197
+ parsed = parseFloat(value.replace(/rem$/, "")) * 16;
198
+ } else {
199
+ parsed = parseFloat(value.replace(/px$/, ""));
200
+ }
201
+ if (!isNaN(parsed)) {
202
+ spacingResult[key] = parsed;
203
+ } else {
204
+ if (process.env.NODE_ENV !== "production") {
205
+ console.warn(
206
+ `[react-native-tailwind] Invalid spacing value for "${key}": ${value}. Expected number or string like "16px" or "1rem".`
207
+ );
208
+ }
209
+ }
210
+ }
211
+ }
149
212
  return {
150
213
  colors: flattenColors(colors),
151
214
  fontFamily: fontFamilyResult,
152
- fontSize: fontSizeResult
215
+ fontSize: fontSizeResult,
216
+ spacing: spacingResult
153
217
  };
154
218
  }
155
219
 
@@ -1083,7 +1147,8 @@ var INSET_SCALE = {
1083
1147
  20: 80,
1084
1148
  24: 96
1085
1149
  };
1086
- function parseLayout(cls) {
1150
+ function parseLayout(cls, customSpacing) {
1151
+ const insetMap = customSpacing ? { ...INSET_SCALE, ...customSpacing } : INSET_SCALE;
1087
1152
  if (cls.startsWith("z-")) {
1088
1153
  const zKey = cls.substring(2);
1089
1154
  const arbitraryZ = parseArbitraryZIndex(zKey);
@@ -1104,7 +1169,7 @@ function parseLayout(cls) {
1104
1169
  if (arbitraryTop !== null) {
1105
1170
  return { top: arbitraryTop };
1106
1171
  }
1107
- const topValue = INSET_SCALE[topKey];
1172
+ const topValue = insetMap[topKey];
1108
1173
  if (topValue !== void 0) {
1109
1174
  return { top: topValue };
1110
1175
  }
@@ -1118,7 +1183,7 @@ function parseLayout(cls) {
1118
1183
  if (arbitraryRight !== null) {
1119
1184
  return { right: arbitraryRight };
1120
1185
  }
1121
- const rightValue = INSET_SCALE[rightKey];
1186
+ const rightValue = insetMap[rightKey];
1122
1187
  if (rightValue !== void 0) {
1123
1188
  return { right: rightValue };
1124
1189
  }
@@ -1132,7 +1197,7 @@ function parseLayout(cls) {
1132
1197
  if (arbitraryBottom !== null) {
1133
1198
  return { bottom: arbitraryBottom };
1134
1199
  }
1135
- const bottomValue = INSET_SCALE[bottomKey];
1200
+ const bottomValue = insetMap[bottomKey];
1136
1201
  if (bottomValue !== void 0) {
1137
1202
  return { bottom: bottomValue };
1138
1203
  }
@@ -1146,7 +1211,7 @@ function parseLayout(cls) {
1146
1211
  if (arbitraryLeft !== null) {
1147
1212
  return { left: arbitraryLeft };
1148
1213
  }
1149
- const leftValue = INSET_SCALE[leftKey];
1214
+ const leftValue = insetMap[leftKey];
1150
1215
  if (leftValue !== void 0) {
1151
1216
  return { left: leftValue };
1152
1217
  }
@@ -1169,7 +1234,7 @@ function parseLayout(cls) {
1169
1234
  }
1170
1235
  return { start: arbitraryStart };
1171
1236
  }
1172
- const startValue = INSET_SCALE[startKey];
1237
+ const startValue = insetMap[startKey];
1173
1238
  if (startValue !== void 0) {
1174
1239
  return { start: isNegative ? -startValue : startValue };
1175
1240
  }
@@ -1192,7 +1257,7 @@ function parseLayout(cls) {
1192
1257
  }
1193
1258
  return { end: arbitraryEnd };
1194
1259
  }
1195
- const endValue = INSET_SCALE[endKey];
1260
+ const endValue = insetMap[endKey];
1196
1261
  if (endValue !== void 0) {
1197
1262
  return { end: isNegative ? -endValue : endValue };
1198
1263
  }
@@ -1203,7 +1268,7 @@ function parseLayout(cls) {
1203
1268
  if (arbitraryInset !== null) {
1204
1269
  return { left: arbitraryInset, right: arbitraryInset };
1205
1270
  }
1206
- const insetValue = INSET_SCALE[insetKey];
1271
+ const insetValue = insetMap[insetKey];
1207
1272
  if (insetValue !== void 0) {
1208
1273
  return { left: insetValue, right: insetValue };
1209
1274
  }
@@ -1214,7 +1279,7 @@ function parseLayout(cls) {
1214
1279
  if (arbitraryInset !== null) {
1215
1280
  return { top: arbitraryInset, bottom: arbitraryInset };
1216
1281
  }
1217
- const insetValue = INSET_SCALE[insetKey];
1282
+ const insetValue = insetMap[insetKey];
1218
1283
  if (insetValue !== void 0) {
1219
1284
  return { top: insetValue, bottom: insetValue };
1220
1285
  }
@@ -1225,7 +1290,7 @@ function parseLayout(cls) {
1225
1290
  if (arbitraryInset !== null) {
1226
1291
  return { start: arbitraryInset };
1227
1292
  }
1228
- const insetValue = INSET_SCALE[insetKey];
1293
+ const insetValue = insetMap[insetKey];
1229
1294
  if (insetValue !== void 0) {
1230
1295
  return { start: insetValue };
1231
1296
  }
@@ -1236,7 +1301,7 @@ function parseLayout(cls) {
1236
1301
  if (arbitraryInset !== null) {
1237
1302
  return { end: arbitraryInset };
1238
1303
  }
1239
- const insetValue = INSET_SCALE[insetKey];
1304
+ const insetValue = insetMap[insetKey];
1240
1305
  if (insetValue !== void 0) {
1241
1306
  return { end: insetValue };
1242
1307
  }
@@ -1247,7 +1312,7 @@ function parseLayout(cls) {
1247
1312
  if (arbitraryInset !== null) {
1248
1313
  return { top: arbitraryInset, right: arbitraryInset, bottom: arbitraryInset, left: arbitraryInset };
1249
1314
  }
1250
- const insetValue = INSET_SCALE[insetKey];
1315
+ const insetValue = insetMap[insetKey];
1251
1316
  if (insetValue !== void 0) {
1252
1317
  return { top: insetValue, right: insetValue, bottom: insetValue, left: insetValue };
1253
1318
  }
@@ -1407,7 +1472,8 @@ function parseArbitrarySize(value) {
1407
1472
  }
1408
1473
  return null;
1409
1474
  }
1410
- function parseSizing(cls) {
1475
+ function parseSizing(cls, customSpacing) {
1476
+ const sizeMap = customSpacing ? { ...SIZE_SCALE, ...customSpacing } : SIZE_SCALE;
1411
1477
  if (cls.startsWith("w-")) {
1412
1478
  const sizeKey = cls.substring(2);
1413
1479
  if (sizeKey === "screen") {
@@ -1421,7 +1487,7 @@ function parseSizing(cls) {
1421
1487
  if (percentage) {
1422
1488
  return { width: percentage };
1423
1489
  }
1424
- const numericSize = SIZE_SCALE[sizeKey];
1490
+ const numericSize = sizeMap[sizeKey];
1425
1491
  if (numericSize !== void 0) {
1426
1492
  return { width: numericSize };
1427
1493
  }
@@ -1442,7 +1508,7 @@ function parseSizing(cls) {
1442
1508
  if (percentage) {
1443
1509
  return { height: percentage };
1444
1510
  }
1445
- const numericSize = SIZE_SCALE[sizeKey];
1511
+ const numericSize = sizeMap[sizeKey];
1446
1512
  if (numericSize !== void 0) {
1447
1513
  return { height: numericSize };
1448
1514
  }
@@ -1460,7 +1526,7 @@ function parseSizing(cls) {
1460
1526
  if (percentage) {
1461
1527
  return { minWidth: percentage };
1462
1528
  }
1463
- const numericSize = SIZE_SCALE[sizeKey];
1529
+ const numericSize = sizeMap[sizeKey];
1464
1530
  if (numericSize !== void 0) {
1465
1531
  return { minWidth: numericSize };
1466
1532
  }
@@ -1475,7 +1541,7 @@ function parseSizing(cls) {
1475
1541
  if (percentage) {
1476
1542
  return { minHeight: percentage };
1477
1543
  }
1478
- const numericSize = SIZE_SCALE[sizeKey];
1544
+ const numericSize = sizeMap[sizeKey];
1479
1545
  if (numericSize !== void 0) {
1480
1546
  return { minHeight: numericSize };
1481
1547
  }
@@ -1490,7 +1556,7 @@ function parseSizing(cls) {
1490
1556
  if (percentage) {
1491
1557
  return { maxWidth: percentage };
1492
1558
  }
1493
- const numericSize = SIZE_SCALE[sizeKey];
1559
+ const numericSize = sizeMap[sizeKey];
1494
1560
  if (numericSize !== void 0) {
1495
1561
  return { maxWidth: numericSize };
1496
1562
  }
@@ -1505,7 +1571,7 @@ function parseSizing(cls) {
1505
1571
  if (percentage) {
1506
1572
  return { maxHeight: percentage };
1507
1573
  }
1508
- const numericSize = SIZE_SCALE[sizeKey];
1574
+ const numericSize = sizeMap[sizeKey];
1509
1575
  if (numericSize !== void 0) {
1510
1576
  return { maxHeight: numericSize };
1511
1577
  }
@@ -1565,7 +1631,8 @@ function parseArbitrarySpacing(value) {
1565
1631
  }
1566
1632
  return null;
1567
1633
  }
1568
- function parseSpacing(cls) {
1634
+ function parseSpacing(cls, customSpacing) {
1635
+ const spacingMap = customSpacing ? { ...SPACING_SCALE, ...customSpacing } : SPACING_SCALE;
1569
1636
  const marginMatch = cls.match(/^(-?)m([xytrblse]?)-(.+)$/);
1570
1637
  if (marginMatch) {
1571
1638
  const [, negativePrefix, dir, valueStr] = marginMatch;
@@ -1575,7 +1642,7 @@ function parseSpacing(cls) {
1575
1642
  const finalValue = isNegative ? -arbitraryValue : arbitraryValue;
1576
1643
  return getMarginStyle(dir, finalValue);
1577
1644
  }
1578
- const scaleValue = SPACING_SCALE[valueStr];
1645
+ const scaleValue = spacingMap[valueStr];
1579
1646
  if (scaleValue !== void 0) {
1580
1647
  const finalValue = isNegative ? -scaleValue : scaleValue;
1581
1648
  return getMarginStyle(dir, finalValue);
@@ -1588,7 +1655,7 @@ function parseSpacing(cls) {
1588
1655
  if (arbitraryValue !== null) {
1589
1656
  return getPaddingStyle(dir, arbitraryValue);
1590
1657
  }
1591
- const scaleValue = SPACING_SCALE[valueStr];
1658
+ const scaleValue = spacingMap[valueStr];
1592
1659
  if (scaleValue !== void 0) {
1593
1660
  return getPaddingStyle(dir, scaleValue);
1594
1661
  }
@@ -1600,7 +1667,7 @@ function parseSpacing(cls) {
1600
1667
  if (arbitraryValue !== null) {
1601
1668
  return { gap: arbitraryValue };
1602
1669
  }
1603
- const scaleValue = SPACING_SCALE[valueStr];
1670
+ const scaleValue = spacingMap[valueStr];
1604
1671
  if (scaleValue !== void 0) {
1605
1672
  return { gap: scaleValue };
1606
1673
  }
@@ -1766,7 +1833,8 @@ function parseArbitraryPerspective(value) {
1766
1833
  }
1767
1834
  return null;
1768
1835
  }
1769
- function parseTransform(cls) {
1836
+ function parseTransform(cls, customSpacing) {
1837
+ const spacingMap = customSpacing ? { ...SPACING_SCALE, ...customSpacing } : SPACING_SCALE;
1770
1838
  if (cls.startsWith("origin-")) {
1771
1839
  if (process.env.NODE_ENV !== "production") {
1772
1840
  console.warn(
@@ -1872,7 +1940,7 @@ function parseTransform(cls) {
1872
1940
  const value = typeof arbitraryTranslate === "number" ? isNegative ? -arbitraryTranslate : arbitraryTranslate : isNegative ? `-${arbitraryTranslate}` : arbitraryTranslate;
1873
1941
  return { transform: [{ translateX: value }] };
1874
1942
  }
1875
- const translateValue = SPACING_SCALE[translateKey];
1943
+ const translateValue = spacingMap[translateKey];
1876
1944
  if (translateValue !== void 0) {
1877
1945
  const value = isNegative ? -translateValue : translateValue;
1878
1946
  return { transform: [{ translateX: value }] };
@@ -1886,7 +1954,7 @@ function parseTransform(cls) {
1886
1954
  const value = typeof arbitraryTranslate === "number" ? isNegative ? -arbitraryTranslate : arbitraryTranslate : isNegative ? `-${arbitraryTranslate}` : arbitraryTranslate;
1887
1955
  return { transform: [{ translateY: value }] };
1888
1956
  }
1889
- const translateValue = SPACING_SCALE[translateKey];
1957
+ const translateValue = spacingMap[translateKey];
1890
1958
  if (translateValue !== void 0) {
1891
1959
  const value = isNegative ? -translateValue : translateValue;
1892
1960
  return { transform: [{ translateY: value }] };
@@ -2267,15 +2335,15 @@ function parseClassName(className, customTheme) {
2267
2335
  }
2268
2336
  function parseClass(cls, customTheme) {
2269
2337
  const parsers = [
2270
- parseSpacing,
2338
+ (cls2) => parseSpacing(cls2, customTheme?.spacing),
2271
2339
  (cls2) => parseBorder(cls2, customTheme?.colors),
2272
2340
  (cls2) => parseColor(cls2, customTheme?.colors),
2273
- parseLayout,
2341
+ (cls2) => parseLayout(cls2, customTheme?.spacing),
2274
2342
  (cls2) => parseTypography(cls2, customTheme?.fontFamily, customTheme?.fontSize),
2275
- parseSizing,
2343
+ (cls2) => parseSizing(cls2, customTheme?.spacing),
2276
2344
  parseShadow,
2277
2345
  parseAspectRatio,
2278
- parseTransform
2346
+ (cls2) => parseTransform(cls2, customTheme?.spacing)
2279
2347
  ];
2280
2348
  for (const parser of parsers) {
2281
2349
  const result = parser(cls);
@@ -10,6 +10,7 @@ export type CustomTheme = {
10
10
  colors?: Record<string, string>;
11
11
  fontFamily?: Record<string, string>;
12
12
  fontSize?: Record<string, number>;
13
+ spacing?: Record<string, number>;
13
14
  };
14
15
  /**
15
16
  * Parse a className string and return a React Native style object