@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
@@ -95,8 +95,13 @@ function parseArbitrarySize(value: string): number | string | null {
95
95
 
96
96
  /**
97
97
  * Parse sizing classes
98
+ * @param cls - The class name to parse
99
+ * @param customSpacing - Optional custom spacing values from tailwind.config (shared with spacing utilities)
98
100
  */
99
- export function parseSizing(cls: string): StyleObject | null {
101
+ export function parseSizing(cls: string, customSpacing?: Record<string, number>): StyleObject | null {
102
+ // Merge custom spacing with defaults (custom takes precedence)
103
+ const sizeMap = customSpacing ? { ...SIZE_SCALE, ...customSpacing } : SIZE_SCALE;
104
+
100
105
  // Width
101
106
  if (cls.startsWith("w-")) {
102
107
  const sizeKey = cls.substring(2);
@@ -106,7 +111,7 @@ export function parseSizing(cls: string): StyleObject | null {
106
111
  return { width: `${RUNTIME_DIMENSIONS_MARKER}width}}` } as StyleObject;
107
112
  }
108
113
 
109
- // Arbitrary values: w-[123px], w-[50%]
114
+ // Arbitrary values: w-[123px], w-[50%] (highest priority)
110
115
  const arbitrarySize = parseArbitrarySize(sizeKey);
111
116
  if (arbitrarySize !== null) {
112
117
  return { width: arbitrarySize };
@@ -118,8 +123,8 @@ export function parseSizing(cls: string): StyleObject | null {
118
123
  return { width: percentage };
119
124
  }
120
125
 
121
- // Numeric widths: w-4, w-8, etc.
122
- const numericSize = SIZE_SCALE[sizeKey];
126
+ // Numeric widths: w-4, w-8, etc. (includes custom spacing)
127
+ const numericSize = sizeMap[sizeKey];
123
128
  if (numericSize !== undefined) {
124
129
  return { width: numericSize };
125
130
  }
@@ -139,7 +144,7 @@ export function parseSizing(cls: string): StyleObject | null {
139
144
  return { height: `${RUNTIME_DIMENSIONS_MARKER}height}}` } as StyleObject;
140
145
  }
141
146
 
142
- // Arbitrary values: h-[123px], h-[50%]
147
+ // Arbitrary values: h-[123px], h-[50%] (highest priority)
143
148
  const arbitrarySize = parseArbitrarySize(sizeKey);
144
149
  if (arbitrarySize !== null) {
145
150
  return { height: arbitrarySize };
@@ -151,8 +156,8 @@ export function parseSizing(cls: string): StyleObject | null {
151
156
  return { height: percentage };
152
157
  }
153
158
 
154
- // Numeric heights: h-4, h-8, etc.
155
- const numericSize = SIZE_SCALE[sizeKey];
159
+ // Numeric heights: h-4, h-8, etc. (includes custom spacing)
160
+ const numericSize = sizeMap[sizeKey];
156
161
  if (numericSize !== undefined) {
157
162
  return { height: numericSize };
158
163
  }
@@ -167,7 +172,7 @@ export function parseSizing(cls: string): StyleObject | null {
167
172
  if (cls.startsWith("min-w-")) {
168
173
  const sizeKey = cls.substring(6);
169
174
 
170
- // Arbitrary values: min-w-[123px], min-w-[50%]
175
+ // Arbitrary values: min-w-[123px], min-w-[50%] (highest priority)
171
176
  const arbitrarySize = parseArbitrarySize(sizeKey);
172
177
  if (arbitrarySize !== null) {
173
178
  return { minWidth: arbitrarySize };
@@ -178,7 +183,7 @@ export function parseSizing(cls: string): StyleObject | null {
178
183
  return { minWidth: percentage };
179
184
  }
180
185
 
181
- const numericSize = SIZE_SCALE[sizeKey];
186
+ const numericSize = sizeMap[sizeKey];
182
187
  if (numericSize !== undefined) {
183
188
  return { minWidth: numericSize };
184
189
  }
@@ -188,7 +193,7 @@ export function parseSizing(cls: string): StyleObject | null {
188
193
  if (cls.startsWith("min-h-")) {
189
194
  const sizeKey = cls.substring(6);
190
195
 
191
- // Arbitrary values: min-h-[123px], min-h-[50%]
196
+ // Arbitrary values: min-h-[123px], min-h-[50%] (highest priority)
192
197
  const arbitrarySize = parseArbitrarySize(sizeKey);
193
198
  if (arbitrarySize !== null) {
194
199
  return { minHeight: arbitrarySize };
@@ -199,7 +204,7 @@ export function parseSizing(cls: string): StyleObject | null {
199
204
  return { minHeight: percentage };
200
205
  }
201
206
 
202
- const numericSize = SIZE_SCALE[sizeKey];
207
+ const numericSize = sizeMap[sizeKey];
203
208
  if (numericSize !== undefined) {
204
209
  return { minHeight: numericSize };
205
210
  }
@@ -209,7 +214,7 @@ export function parseSizing(cls: string): StyleObject | null {
209
214
  if (cls.startsWith("max-w-")) {
210
215
  const sizeKey = cls.substring(6);
211
216
 
212
- // Arbitrary values: max-w-[123px], max-w-[50%]
217
+ // Arbitrary values: max-w-[123px], max-w-[50%] (highest priority)
213
218
  const arbitrarySize = parseArbitrarySize(sizeKey);
214
219
  if (arbitrarySize !== null) {
215
220
  return { maxWidth: arbitrarySize };
@@ -220,7 +225,7 @@ export function parseSizing(cls: string): StyleObject | null {
220
225
  return { maxWidth: percentage };
221
226
  }
222
227
 
223
- const numericSize = SIZE_SCALE[sizeKey];
228
+ const numericSize = sizeMap[sizeKey];
224
229
  if (numericSize !== undefined) {
225
230
  return { maxWidth: numericSize };
226
231
  }
@@ -230,7 +235,7 @@ export function parseSizing(cls: string): StyleObject | null {
230
235
  if (cls.startsWith("max-h-")) {
231
236
  const sizeKey = cls.substring(6);
232
237
 
233
- // Arbitrary values: max-h-[123px], max-h-[50%]
238
+ // Arbitrary values: max-h-[123px], max-h-[50%] (highest priority)
234
239
  const arbitrarySize = parseArbitrarySize(sizeKey);
235
240
  if (arbitrarySize !== null) {
236
241
  return { maxHeight: arbitrarySize };
@@ -241,7 +246,7 @@ export function parseSizing(cls: string): StyleObject | null {
241
246
  return { maxHeight: percentage };
242
247
  }
243
248
 
244
- const numericSize = SIZE_SCALE[sizeKey];
249
+ const numericSize = sizeMap[sizeKey];
245
250
  if (numericSize !== undefined) {
246
251
  return { maxHeight: numericSize };
247
252
  }
@@ -415,3 +415,60 @@ describe("parseSpacing - logical padding (RTL-aware)", () => {
415
415
  expect(parseSpacing("pe-[24]")).toEqual({ paddingEnd: 24 });
416
416
  });
417
417
  });
418
+
419
+ describe("parseSpacing - custom spacing", () => {
420
+ const customSpacing = {
421
+ xs: 4,
422
+ sm: 8,
423
+ md: 16,
424
+ lg: 32,
425
+ xl: 64,
426
+ "4": 20, // Override default (16)
427
+ };
428
+
429
+ it("should support custom spacing values for margin", () => {
430
+ expect(parseSpacing("m-xs", customSpacing)).toEqual({ margin: 4 });
431
+ expect(parseSpacing("m-sm", customSpacing)).toEqual({ margin: 8 });
432
+ expect(parseSpacing("m-lg", customSpacing)).toEqual({ margin: 32 });
433
+ expect(parseSpacing("mx-xl", customSpacing)).toEqual({ marginHorizontal: 64 });
434
+ expect(parseSpacing("mt-md", customSpacing)).toEqual({ marginTop: 16 });
435
+ });
436
+
437
+ it("should support custom spacing values for padding", () => {
438
+ expect(parseSpacing("p-xs", customSpacing)).toEqual({ padding: 4 });
439
+ expect(parseSpacing("p-sm", customSpacing)).toEqual({ padding: 8 });
440
+ expect(parseSpacing("px-lg", customSpacing)).toEqual({ paddingHorizontal: 32 });
441
+ expect(parseSpacing("pt-xl", customSpacing)).toEqual({ paddingTop: 64 });
442
+ });
443
+
444
+ it("should support custom spacing values for gap", () => {
445
+ expect(parseSpacing("gap-xs", customSpacing)).toEqual({ gap: 4 });
446
+ expect(parseSpacing("gap-md", customSpacing)).toEqual({ gap: 16 });
447
+ expect(parseSpacing("gap-xl", customSpacing)).toEqual({ gap: 64 });
448
+ });
449
+
450
+ it("should allow custom spacing to override preset values", () => {
451
+ expect(parseSpacing("m-4", customSpacing)).toEqual({ margin: 20 }); // Custom 20, not default 16
452
+ });
453
+
454
+ it("should prefer arbitrary values over custom spacing", () => {
455
+ expect(parseSpacing("m-[24px]", customSpacing)).toEqual({ margin: 24 }); // Arbitrary wins
456
+ expect(parseSpacing("p-[50]", customSpacing)).toEqual({ padding: 50 }); // Arbitrary wins
457
+ });
458
+
459
+ it("should support negative margins with custom spacing", () => {
460
+ expect(parseSpacing("-m-xs", customSpacing)).toEqual({ margin: -4 });
461
+ expect(parseSpacing("-m-lg", customSpacing)).toEqual({ margin: -32 });
462
+ expect(parseSpacing("-mx-xl", customSpacing)).toEqual({ marginHorizontal: -64 });
463
+ });
464
+
465
+ it("should fall back to preset scale for unknown custom keys", () => {
466
+ expect(parseSpacing("m-8", customSpacing)).toEqual({ margin: 32 }); // Not overridden, uses preset
467
+ expect(parseSpacing("p-12", customSpacing)).toEqual({ padding: 48 }); // Not overridden, uses preset
468
+ });
469
+
470
+ it("should work without custom spacing (backward compatible)", () => {
471
+ expect(parseSpacing("m-4")).toEqual({ margin: 16 }); // Default behavior
472
+ expect(parseSpacing("p-8")).toEqual({ padding: 32 }); // Default behavior
473
+ });
474
+ });
@@ -70,8 +70,13 @@ function parseArbitrarySpacing(value: string): number | null {
70
70
  /**
71
71
  * Parse spacing classes (margin, padding, gap)
72
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], ms-4, pe-2
73
+ * @param cls - The class name to parse
74
+ * @param customSpacing - Optional custom spacing values from tailwind.config
73
75
  */
74
- export function parseSpacing(cls: string): StyleObject | null {
76
+ export function parseSpacing(cls: string, customSpacing?: Record<string, number>): StyleObject | null {
77
+ // Merge custom spacing with defaults (custom takes precedence)
78
+ const spacingMap = customSpacing ? { ...SPACING_SCALE, ...customSpacing } : SPACING_SCALE;
79
+
75
80
  // Margin: m-4, mx-2, mt-8, ms-4, me-2, m-[16px], -m-4, -mt-2, etc.
76
81
  // Supports negative values for margins (but not padding or gap)
77
82
  // s = start (RTL-aware), e = end (RTL-aware)
@@ -80,15 +85,15 @@ export function parseSpacing(cls: string): StyleObject | null {
80
85
  const [, negativePrefix, dir, valueStr] = marginMatch;
81
86
  const isNegative = negativePrefix === "-";
82
87
 
83
- // Try arbitrary value first
88
+ // Try arbitrary value first (highest priority)
84
89
  const arbitraryValue = parseArbitrarySpacing(valueStr);
85
90
  if (arbitraryValue !== null) {
86
91
  const finalValue = isNegative ? -arbitraryValue : arbitraryValue;
87
92
  return getMarginStyle(dir, finalValue);
88
93
  }
89
94
 
90
- // Try preset scale
91
- const scaleValue = SPACING_SCALE[valueStr];
95
+ // Try spacing scale (includes custom spacing)
96
+ const scaleValue = spacingMap[valueStr];
92
97
  if (scaleValue !== undefined) {
93
98
  const finalValue = isNegative ? -scaleValue : scaleValue;
94
99
  return getMarginStyle(dir, finalValue);
@@ -101,14 +106,14 @@ export function parseSpacing(cls: string): StyleObject | null {
101
106
  if (paddingMatch) {
102
107
  const [, dir, valueStr] = paddingMatch;
103
108
 
104
- // Try arbitrary value first
109
+ // Try arbitrary value first (highest priority)
105
110
  const arbitraryValue = parseArbitrarySpacing(valueStr);
106
111
  if (arbitraryValue !== null) {
107
112
  return getPaddingStyle(dir, arbitraryValue);
108
113
  }
109
114
 
110
- // Try preset scale
111
- const scaleValue = SPACING_SCALE[valueStr];
115
+ // Try spacing scale (includes custom spacing)
116
+ const scaleValue = spacingMap[valueStr];
112
117
  if (scaleValue !== undefined) {
113
118
  return getPaddingStyle(dir, scaleValue);
114
119
  }
@@ -119,14 +124,14 @@ export function parseSpacing(cls: string): StyleObject | null {
119
124
  if (gapMatch) {
120
125
  const valueStr = gapMatch[1];
121
126
 
122
- // Try arbitrary value first
127
+ // Try arbitrary value first (highest priority)
123
128
  const arbitraryValue = parseArbitrarySpacing(valueStr);
124
129
  if (arbitraryValue !== null) {
125
130
  return { gap: arbitraryValue };
126
131
  }
127
132
 
128
- // Try preset scale
129
- const scaleValue = SPACING_SCALE[valueStr];
133
+ // Try spacing scale (includes custom spacing)
134
+ const scaleValue = spacingMap[valueStr];
130
135
  if (scaleValue !== undefined) {
131
136
  return { gap: scaleValue };
132
137
  }
@@ -316,3 +316,60 @@ describe("parseTransform - case sensitivity", () => {
316
316
  expect(parseTransform("ROTATE-45")).toBeNull();
317
317
  });
318
318
  });
319
+
320
+ describe("parseTransform - custom spacing", () => {
321
+ const customSpacing = {
322
+ xs: 4,
323
+ sm: 8,
324
+ md: 16,
325
+ lg: 32,
326
+ xl: 64,
327
+ "4": 20, // Override default (16)
328
+ };
329
+
330
+ it("should support custom spacing values for translateX", () => {
331
+ expect(parseTransform("translate-x-xs", customSpacing)).toEqual({ transform: [{ translateX: 4 }] });
332
+ expect(parseTransform("translate-x-sm", customSpacing)).toEqual({ transform: [{ translateX: 8 }] });
333
+ expect(parseTransform("translate-x-lg", customSpacing)).toEqual({ transform: [{ translateX: 32 }] });
334
+ expect(parseTransform("translate-x-xl", customSpacing)).toEqual({ transform: [{ translateX: 64 }] });
335
+ });
336
+
337
+ it("should support custom spacing values for translateY", () => {
338
+ expect(parseTransform("translate-y-xs", customSpacing)).toEqual({ transform: [{ translateY: 4 }] });
339
+ expect(parseTransform("translate-y-md", customSpacing)).toEqual({ transform: [{ translateY: 16 }] });
340
+ expect(parseTransform("translate-y-xl", customSpacing)).toEqual({ transform: [{ translateY: 64 }] });
341
+ });
342
+
343
+ it("should support negative custom spacing for translate", () => {
344
+ expect(parseTransform("-translate-x-sm", customSpacing)).toEqual({ transform: [{ translateX: -8 }] });
345
+ expect(parseTransform("-translate-y-lg", customSpacing)).toEqual({ transform: [{ translateY: -32 }] });
346
+ });
347
+
348
+ it("should allow custom spacing to override preset values", () => {
349
+ expect(parseTransform("translate-x-4", customSpacing)).toEqual({ transform: [{ translateX: 20 }] }); // Custom 20, not default 16
350
+ expect(parseTransform("translate-y-4", customSpacing)).toEqual({ transform: [{ translateY: 20 }] }); // Custom 20, not default 16
351
+ });
352
+
353
+ it("should prefer arbitrary values over custom spacing", () => {
354
+ expect(parseTransform("translate-x-[24px]", customSpacing)).toEqual({ transform: [{ translateX: 24 }] }); // Arbitrary wins
355
+ expect(parseTransform("translate-y-[50]", customSpacing)).toEqual({ transform: [{ translateY: 50 }] }); // Arbitrary wins
356
+ });
357
+
358
+ it("should fall back to preset scale for unknown custom keys", () => {
359
+ expect(parseTransform("translate-x-8", customSpacing)).toEqual({ transform: [{ translateX: 32 }] }); // Not overridden, uses preset
360
+ expect(parseTransform("translate-y-12", customSpacing)).toEqual({ transform: [{ translateY: 48 }] }); // Not overridden, uses preset
361
+ });
362
+
363
+ it("should work without custom spacing (backward compatible)", () => {
364
+ expect(parseTransform("translate-x-4")).toEqual({ transform: [{ translateX: 16 }] }); // Default behavior
365
+ expect(parseTransform("translate-y-8")).toEqual({ transform: [{ translateY: 32 }] }); // Default behavior
366
+ });
367
+
368
+ it("should not affect non-translate transforms", () => {
369
+ // Scale, rotate, skew, perspective should not use custom spacing
370
+ expect(parseTransform("scale-110", customSpacing)).toEqual({ transform: [{ scale: 1.1 }] });
371
+ expect(parseTransform("rotate-45", customSpacing)).toEqual({ transform: [{ rotate: "45deg" }] });
372
+ expect(parseTransform("skew-x-6", customSpacing)).toEqual({ transform: [{ skewX: "6deg" }] });
373
+ expect(parseTransform("perspective-500", customSpacing)).toEqual({ transform: [{ perspective: 500 }] });
374
+ });
375
+ });
@@ -164,8 +164,12 @@ function parseArbitraryPerspective(value: string): number | null {
164
164
  /**
165
165
  * Parse transform classes
166
166
  * Each transform class returns a transform array with a single transform object
167
+ * @param cls - The class name to parse
168
+ * @param customSpacing - Optional custom spacing values from tailwind.config (for translate utilities)
167
169
  */
168
- export function parseTransform(cls: string): StyleObject | null {
170
+ export function parseTransform(cls: string, customSpacing?: Record<string, number>): StyleObject | null {
171
+ // Merge custom spacing with defaults for translate utilities
172
+ const spacingMap = customSpacing ? { ...SPACING_SCALE, ...customSpacing } : SPACING_SCALE;
169
173
  // Transform origin warning (not supported in React Native)
170
174
  if (cls.startsWith("origin-")) {
171
175
  /* v8 ignore next 5 */
@@ -320,7 +324,7 @@ export function parseTransform(cls: string): StyleObject | null {
320
324
  return { transform: [{ translateX: value }] };
321
325
  }
322
326
 
323
- const translateValue = SPACING_SCALE[translateKey];
327
+ const translateValue = spacingMap[translateKey];
324
328
  if (translateValue !== undefined) {
325
329
  const value = isNegative ? -translateValue : translateValue;
326
330
  return { transform: [{ translateX: value }] };
@@ -346,7 +350,7 @@ export function parseTransform(cls: string): StyleObject | null {
346
350
  return { transform: [{ translateY: value }] };
347
351
  }
348
352
 
349
- const translateValue = SPACING_SCALE[translateKey];
353
+ const translateValue = spacingMap[translateKey];
350
354
  if (translateValue !== undefined) {
351
355
  const value = isNegative ? -translateValue : translateValue;
352
356
  return { transform: [{ translateY: value }] };
@@ -209,6 +209,155 @@ describe("runtime", () => {
209
209
  backgroundColor: "#007AFF",
210
210
  });
211
211
  });
212
+
213
+ it("should set custom fontFamily", () => {
214
+ setConfig({
215
+ theme: {
216
+ extend: {
217
+ fontFamily: {
218
+ display: "Inter",
219
+ body: ["Roboto", "sans-serif"], // Array format - takes first value
220
+ },
221
+ },
222
+ },
223
+ });
224
+
225
+ const theme = getCustomTheme();
226
+ expect(theme.fontFamily).toEqual({
227
+ display: "Inter",
228
+ body: "Roboto",
229
+ });
230
+ });
231
+
232
+ it("should set custom fontSize", () => {
233
+ setConfig({
234
+ theme: {
235
+ extend: {
236
+ fontSize: {
237
+ tiny: 10,
238
+ small: "12px",
239
+ medium: 16,
240
+ large: "24",
241
+ },
242
+ },
243
+ },
244
+ });
245
+
246
+ const theme = getCustomTheme();
247
+ expect(theme.fontSize).toEqual({
248
+ tiny: 10,
249
+ small: 12,
250
+ medium: 16,
251
+ large: 24,
252
+ });
253
+ });
254
+
255
+ it("should set custom spacing", () => {
256
+ setConfig({
257
+ theme: {
258
+ extend: {
259
+ spacing: {
260
+ xs: 4,
261
+ sm: "8px",
262
+ md: 16,
263
+ lg: "2rem", // 2rem = 32px
264
+ },
265
+ },
266
+ },
267
+ });
268
+
269
+ const theme = getCustomTheme();
270
+ expect(theme.spacing).toEqual({
271
+ xs: 4,
272
+ sm: 8,
273
+ md: 16,
274
+ lg: 32,
275
+ });
276
+ });
277
+
278
+ it("should use custom fontSize in parsing", () => {
279
+ setConfig({
280
+ theme: {
281
+ extend: {
282
+ fontSize: {
283
+ tiny: 10,
284
+ },
285
+ },
286
+ },
287
+ });
288
+
289
+ const result = tw`text-tiny`;
290
+ expect(result?.style).toEqual({
291
+ fontSize: 10,
292
+ });
293
+ });
294
+
295
+ it("should use custom spacing in parsing", () => {
296
+ setConfig({
297
+ theme: {
298
+ extend: {
299
+ spacing: {
300
+ xs: 4,
301
+ },
302
+ },
303
+ },
304
+ });
305
+
306
+ const result = tw`m-xs p-xs`;
307
+ expect(result?.style).toEqual({
308
+ margin: 4,
309
+ padding: 4,
310
+ });
311
+ });
312
+
313
+ it("should reset fontSize and spacing when config is cleared", () => {
314
+ setConfig({
315
+ theme: {
316
+ extend: {
317
+ fontSize: { tiny: 10 },
318
+ spacing: { xs: 4 },
319
+ },
320
+ },
321
+ });
322
+
323
+ let theme = getCustomTheme();
324
+ expect(theme.fontSize).toEqual({ tiny: 10 });
325
+ expect(theme.spacing).toEqual({ xs: 4 });
326
+
327
+ setConfig({});
328
+
329
+ theme = getCustomTheme();
330
+ expect(theme.fontSize).toEqual({});
331
+ expect(theme.spacing).toEqual({});
332
+ });
333
+
334
+ it("should handle all theme extensions together", () => {
335
+ setConfig({
336
+ theme: {
337
+ extend: {
338
+ colors: { primary: "#007AFF" },
339
+ fontFamily: { display: "Inter" },
340
+ fontSize: { tiny: 10 },
341
+ spacing: { xs: 4 },
342
+ },
343
+ },
344
+ });
345
+
346
+ const theme = getCustomTheme();
347
+ expect(theme.colors).toEqual({ primary: "#007AFF" });
348
+ expect(theme.fontFamily).toEqual({ display: "Inter" });
349
+ expect(theme.fontSize).toEqual({ tiny: 10 });
350
+ expect(theme.spacing).toEqual({ xs: 4 });
351
+
352
+ // Test that parsing uses all of them
353
+ const result = tw`bg-primary font-display text-tiny m-xs`;
354
+ expect(result?.style).toEqual({
355
+ backgroundColor: "#007AFF",
356
+ fontFamily: "Inter",
357
+ fontSize: 10,
358
+ margin: 4,
359
+ });
360
+ });
212
361
  });
213
362
 
214
363
  describe("cache", () => {
package/src/runtime.ts CHANGED
@@ -12,12 +12,19 @@ export type RuntimeConfig = {
12
12
  extend?: {
13
13
  colors?: Record<string, string | Record<string, string>>;
14
14
  fontFamily?: Record<string, string | string[]>;
15
+ fontSize?: Record<string, string | number>;
16
+ spacing?: Record<string, string | number>;
15
17
  };
16
18
  };
17
19
  };
18
20
 
19
21
  // Global custom theme configuration
20
- const globalCustomTheme: CustomTheme = { colors: {}, fontFamily: {} };
22
+ const globalCustomTheme: CustomTheme = {
23
+ colors: {},
24
+ fontFamily: {},
25
+ fontSize: {},
26
+ spacing: {},
27
+ };
21
28
 
22
29
  // Simple memoization cache
23
30
  const styleCache = new Map<string, TwStyle>();
@@ -72,6 +79,51 @@ export function setConfig(config: RuntimeConfig): void {
72
79
  globalCustomTheme.fontFamily = {};
73
80
  }
74
81
 
82
+ // Extract custom fontSize
83
+ if (config.theme?.extend?.fontSize) {
84
+ const fontSizeResult: Record<string, number> = {};
85
+ for (const [key, value] of Object.entries(config.theme.extend.fontSize)) {
86
+ if (typeof value === "number") {
87
+ fontSizeResult[key] = value;
88
+ } else if (typeof value === "string") {
89
+ // Parse string values like "18px" or "18" to number
90
+ const parsed = parseFloat(value.replace(/px$/, ""));
91
+ if (!isNaN(parsed)) {
92
+ fontSizeResult[key] = parsed;
93
+ }
94
+ }
95
+ }
96
+ globalCustomTheme.fontSize = fontSizeResult;
97
+ } else {
98
+ globalCustomTheme.fontSize = {};
99
+ }
100
+
101
+ // Extract custom spacing
102
+ if (config.theme?.extend?.spacing) {
103
+ const spacingResult: Record<string, number> = {};
104
+ for (const [key, value] of Object.entries(config.theme.extend.spacing)) {
105
+ if (typeof value === "number") {
106
+ spacingResult[key] = value;
107
+ } else if (typeof value === "string") {
108
+ // Parse string values: "18rem" -> 288, "16px" -> 16, "16" -> 16
109
+ let parsed: number;
110
+ if (value.endsWith("rem")) {
111
+ // Convert rem to px (1rem = 16px)
112
+ parsed = parseFloat(value.replace(/rem$/, "")) * 16;
113
+ } else {
114
+ // Parse px or unitless values
115
+ parsed = parseFloat(value.replace(/px$/, ""));
116
+ }
117
+ if (!isNaN(parsed)) {
118
+ spacingResult[key] = parsed;
119
+ }
120
+ }
121
+ }
122
+ globalCustomTheme.spacing = spacingResult;
123
+ } else {
124
+ globalCustomTheme.spacing = {};
125
+ }
126
+
75
127
  // Clear cache when config changes
76
128
  styleCache.clear();
77
129
  }