@mgcrea/react-native-tailwind 0.6.0 → 0.7.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 (57) hide show
  1. package/README.md +437 -10
  2. package/dist/babel/config-loader.ts +1 -23
  3. package/dist/babel/index.cjs +543 -150
  4. package/dist/babel/index.d.ts +27 -2
  5. package/dist/babel/index.test.ts +268 -0
  6. package/dist/babel/index.ts +352 -44
  7. package/dist/components/Pressable.d.ts +2 -0
  8. package/dist/components/TextInput.d.ts +2 -0
  9. package/dist/config/palettes.d.ts +302 -0
  10. package/dist/config/palettes.js +1 -0
  11. package/dist/index.d.ts +3 -0
  12. package/dist/index.js +1 -1
  13. package/dist/parser/__snapshots__/colors.test.js.snap +242 -90
  14. package/dist/parser/__snapshots__/transforms.test.js.snap +58 -0
  15. package/dist/parser/colors.js +1 -1
  16. package/dist/parser/colors.test.js +1 -1
  17. package/dist/parser/layout.js +1 -1
  18. package/dist/parser/layout.test.js +1 -1
  19. package/dist/parser/typography.js +1 -1
  20. package/dist/parser/typography.test.js +1 -1
  21. package/dist/runtime.cjs +2 -0
  22. package/dist/runtime.cjs.map +7 -0
  23. package/dist/runtime.d.ts +139 -0
  24. package/dist/runtime.js +2 -0
  25. package/dist/runtime.js.map +7 -0
  26. package/dist/runtime.test.js +1 -0
  27. package/dist/stubs/tw.d.ts +60 -0
  28. package/dist/stubs/tw.js +1 -0
  29. package/dist/utils/flattenColors.d.ts +16 -0
  30. package/dist/utils/flattenColors.js +1 -0
  31. package/dist/utils/flattenColors.test.js +1 -0
  32. package/dist/utils/modifiers.d.ts +29 -0
  33. package/dist/utils/modifiers.js +1 -0
  34. package/dist/utils/modifiers.test.js +1 -0
  35. package/dist/utils/styleKey.test.js +1 -0
  36. package/package.json +15 -3
  37. package/src/babel/config-loader.ts +1 -23
  38. package/src/babel/index.test.ts +268 -0
  39. package/src/babel/index.ts +352 -44
  40. package/src/components/Pressable.tsx +1 -0
  41. package/src/components/TextInput.tsx +1 -0
  42. package/src/config/palettes.ts +304 -0
  43. package/src/index.ts +5 -0
  44. package/src/parser/colors.test.ts +47 -31
  45. package/src/parser/colors.ts +5 -110
  46. package/src/parser/layout.test.ts +35 -0
  47. package/src/parser/layout.ts +26 -0
  48. package/src/parser/typography.test.ts +10 -0
  49. package/src/parser/typography.ts +8 -0
  50. package/src/runtime.test.ts +325 -0
  51. package/src/runtime.ts +280 -0
  52. package/src/stubs/tw.ts +80 -0
  53. package/src/utils/flattenColors.test.ts +361 -0
  54. package/src/utils/flattenColors.ts +32 -0
  55. package/src/utils/modifiers.test.ts +286 -0
  56. package/src/utils/modifiers.ts +63 -0
  57. package/src/utils/styleKey.test.ts +168 -0
package/README.md CHANGED
@@ -1,13 +1,32 @@
1
- # React Native Tailwind
2
-
3
- <div align="center">
4
-
5
- [![npm version](https://img.shields.io/npm/v/@mgcrea/react-native-tailwind.svg?style=for-the-badge)](https://www.npmjs.com/package/@mgcrea/react-native-tailwind)
6
- [![npm downloads](https://img.shields.io/npm/dt/@mgcrea/react-native-tailwind.svg?style=for-the-badge)](https://www.npmjs.com/package/@mgcrea/react-native-tailwind)
7
- [![license](https://img.shields.io/github/license/mgcrea/react-native-tailwind.svg?style=for-the-badge)](https://github.com/mgcrea/react-native-tailwind/blob/main/LICENSE)
8
- [![build status](https://img.shields.io/github/actions/workflow/status/mgcrea/react-native-tailwind/main.yaml?style=for-the-badge&branch=master)](https://github.com/mgcrea/react-native-tailwind/actions/workflows/main.yaml)
9
-
10
- </div>
1
+ <!-- # React Native Tailwind -->
2
+
3
+ <!-- markdownlint-disable MD033 -->
4
+ <p align="center">
5
+ <a href="https://mgcrea.github.io/react-native-swiftui">
6
+ <img src="./.github/assets/logo.png" alt="logo" width="480" height="300"/>
7
+ </a>
8
+ </p>
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/@mgcrea/react-native-tailwind">
11
+ <img src="https://img.shields.io/npm/v/@mgcrea/react-native-tailwind.svg?style=for-the-badge" alt="npm version" />
12
+ </a>
13
+ <a href="https://www.npmjs.com/package/@mgcrea/react-native-tailwind">
14
+ <img src="https://img.shields.io/npm/dt/@mgcrea/react-native-tailwind.svg?style=for-the-badge" alt="npm total downloads" />
15
+ </a>
16
+ <a href="https://www.npmjs.com/package/@mgcrea/react-native-tailwind">
17
+ <img src="https://img.shields.io/npm/dm/@mgcrea/react-native-tailwind.svg?style=for-the-badge" alt="npm monthly downloads" />
18
+ </a>
19
+ <a href="https://www.npmjs.com/package/@mgcrea/react-native-tailwind">
20
+ <img src="https://img.shields.io/npm/l/@mgcrea/react-native-tailwind.svg?style=for-the-badge" alt="npm license" />
21
+ </a>
22
+ <br />
23
+ <a href="https://github.com/mgcrea/react-native-tailwind/actions/workflows/main.yaml">
24
+ <img src="https://img.shields.io/github/actions/workflow/status/mgcrea/react-native-tailwind/main.yaml?style=for-the-badge&branch=main" alt="build status" />
25
+ </a>
26
+ <a href="https://depfu.com/github/mgcrea/react-native-tailwind">
27
+ <img src="https://img.shields.io/depfu/dependencies/github/mgcrea/react-native-tailwind?style=for-the-badge" alt="dependencies status" />
28
+ </a>
29
+ </p>
11
30
 
12
31
  ## Overview
13
32
 
@@ -24,8 +43,14 @@ Compile-time Tailwind CSS for React Native with zero runtime overhead. Transform
24
43
  - 🎨 **Custom colors** — Extend the default palette via `tailwind.config.*`
25
44
  - 📐 **Arbitrary values** — Use custom sizes and borders: `w-[123px]`, `rounded-[20px]`
26
45
  - 🔀 **Dynamic className** — Conditional styles with hybrid compile-time optimization
46
+ - 🏃 **Runtime option** — Optional `tw` template tag for fully dynamic styling (~25KB)
27
47
  - 🎯 **State modifiers** — `active:`, `hover:`, `focus:`, and `disabled:` modifiers for interactive components
28
48
  - 📜 **Special style props** — Support for `contentContainerClassName`, `columnWrapperClassName`, and more
49
+ - 🎛️ **Custom attributes** — Configure which props to transform with exact matching or glob patterns
50
+
51
+ ## Demo
52
+
53
+ ![demo](./.github/assets/demo.gif)
29
54
 
30
55
  ## Installation
31
56
 
@@ -52,6 +77,28 @@ module.exports = {
52
77
  };
53
78
  ```
54
79
 
80
+ **Advanced:** You can customize which attributes are transformed and the generated styles identifier:
81
+
82
+ ```javascript
83
+ module.exports = {
84
+ presets: ["module:@react-native/babel-preset"],
85
+ plugins: [
86
+ [
87
+ "@mgcrea/react-native-tailwind/babel",
88
+ {
89
+ // Specify which attributes to transform
90
+ // Default: ['className', 'contentContainerClassName', 'columnWrapperClassName', 'ListHeaderComponentClassName', 'ListFooterComponentClassName']
91
+ attributes: ["className", "buttonClassName", "containerClassName"],
92
+
93
+ // Custom identifier for the generated StyleSheet constant
94
+ // Default: '_twStyles'
95
+ stylesIdentifier: "styles",
96
+ },
97
+ ],
98
+ ],
99
+ };
100
+ ```
101
+
55
102
  ### 2. Enable TypeScript Support (TypeScript)
56
103
 
57
104
  Create a type declaration file in your project to enable `className` prop autocomplete:
@@ -278,6 +325,219 @@ The Babel plugin will merge them:
278
325
  </View>
279
326
  ```
280
327
 
328
+ ### Compile-Time `tw` Template Tag
329
+
330
+ For static or compile-time determinable styles, you can use the `tw` template tag that gets transformed by the Babel plugin. This provides **zero runtime overhead** as all styles are compiled to `StyleSheet.create` calls.
331
+
332
+ #### Import
333
+
334
+ ```typescript
335
+ import { tw } from "@mgcrea/react-native-tailwind";
336
+ ```
337
+
338
+ #### Basic Usage
339
+
340
+ ```tsx
341
+ import { View, Text, Pressable } from "react-native";
342
+ import { tw } from "@mgcrea/react-native-tailwind";
343
+
344
+ // Static styles - transformed at compile time
345
+ const containerStyles = tw`flex-1 p-4 bg-gray-100`;
346
+ const buttonStyles = tw`p-4 rounded-lg bg-blue-500`;
347
+ const textStyles = tw`text-white font-bold text-center`;
348
+
349
+ export function Example() {
350
+ return (
351
+ <View style={containerStyles.style}>
352
+ <Pressable style={buttonStyles.style}>
353
+ <Text style={textStyles.style}>Click me</Text>
354
+ </Pressable>
355
+ </View>
356
+ );
357
+ }
358
+ ```
359
+
360
+ #### With State Modifiers
361
+
362
+ The compile-time `tw` supports state modifiers (`active:`, `focus:`, `disabled:`) that return a `TwStyle` object with separate style properties:
363
+
364
+ ```tsx
365
+ import { Pressable, Text } from "react-native";
366
+ import { tw } from "@mgcrea/react-native-tailwind";
367
+
368
+ const buttonStyles = tw`bg-blue-500 active:bg-blue-700 disabled:bg-gray-300`;
369
+
370
+ export function Button({ disabled }) {
371
+ return (
372
+ <Pressable
373
+ disabled={disabled}
374
+ style={(state) => [
375
+ buttonStyles.style,
376
+ state.pressed && buttonStyles.activeStyle,
377
+ disabled && buttonStyles.disabledStyle,
378
+ ]}
379
+ >
380
+ <Text style={tw`text-white font-bold`.style}>Press me</Text>
381
+ </Pressable>
382
+ );
383
+ }
384
+ ```
385
+
386
+ #### Transformation Example
387
+
388
+ The Babel plugin transforms your code at compile time:
389
+
390
+ ```tsx
391
+ // Input
392
+ const styles = tw`bg-blue-500 active:bg-blue-700 m-4`;
393
+
394
+ // Compiled Output
395
+ const styles = {
396
+ style: _twStyles._bg_blue_500_m_4,
397
+ activeStyle: _twStyles._active_bg_blue_700,
398
+ };
399
+
400
+ const _twStyles = StyleSheet.create({
401
+ _bg_blue_500_m_4: { backgroundColor: "#2b7fff", margin: 16 },
402
+ _active_bg_blue_700: { backgroundColor: "#1854d6" },
403
+ });
404
+ ```
405
+
406
+ ### Runtime `tw` Template Tag
407
+
408
+ For cases where you need **fully dynamic styling** (values only known at runtime), you can use the runtime `tw` template tag function. This provides runtime parsing of Tailwind classes with memoization for performance.
409
+
410
+ > **Note:** Use the compile-time `tw` (imported from `@mgcrea/react-native-tailwind`) when possible for zero runtime overhead. Only use the runtime version (imported from `@mgcrea/react-native-tailwind/runtime`) when styles must be determined at runtime.
411
+
412
+ #### Installation
413
+
414
+ The runtime module is already included in the package. Import it separately:
415
+
416
+ ```typescript
417
+ import { tw, setConfig } from "@mgcrea/react-native-tailwind/runtime";
418
+ ```
419
+
420
+ #### Basic Usage
421
+
422
+ ```tsx
423
+ import { View, Text, Pressable } from "react-native";
424
+ import { tw } from "@mgcrea/react-native-tailwind/runtime";
425
+
426
+ export function RuntimeExample() {
427
+ const [isActive, setIsActive] = useState(false);
428
+
429
+ return (
430
+ <View style={tw`flex-1 p-4 bg-gray-100`}>
431
+ <Pressable
432
+ onPress={() => setIsActive(!isActive)}
433
+ style={tw`p-4 rounded-lg ${isActive ? "bg-green-500" : "bg-red-500"}`}
434
+ >
435
+ <Text style={tw`text-white font-bold text-center`}>{isActive ? "Active" : "Inactive"}</Text>
436
+ </Pressable>
437
+ </View>
438
+ );
439
+ }
440
+ ```
441
+
442
+ #### Configuration
443
+
444
+ Configure custom colors and other theme options using `setConfig()`:
445
+
446
+ ```typescript
447
+ import { setConfig } from '@mgcrea/react-native-tailwind/runtime';
448
+
449
+ // Match your tailwind.config structure
450
+ setConfig({
451
+ theme: {
452
+ extend: {
453
+ colors: {
454
+ primary: '#007AFF',
455
+ secondary: '#5856D6',
456
+ brand: {
457
+ light: '#FF6B6B',
458
+ dark: '#CC0000',
459
+ },
460
+ },
461
+ },
462
+ },
463
+ });
464
+
465
+ // Now you can use custom colors
466
+ <View style={tw`bg-primary p-4`} />
467
+ <Text style={tw`text-brand-light`}>Custom color</Text>
468
+ ```
469
+
470
+ #### API Reference
471
+
472
+ **`tw` tagged template**
473
+
474
+ ```typescript
475
+ function tw(strings: TemplateStringsArray, ...values: unknown[]): TwStyle;
476
+
477
+ type TwStyle = {
478
+ style: ViewStyle | TextStyle | ImageStyle;
479
+ activeStyle?: ViewStyle | TextStyle | ImageStyle;
480
+ focusStyle?: ViewStyle | TextStyle | ImageStyle;
481
+ disabledStyle?: ViewStyle | TextStyle | ImageStyle;
482
+ };
483
+ ```
484
+
485
+ Parses Tailwind classes at runtime and returns a `TwStyle` object with separate properties for base styles and state modifiers. Results are automatically memoized for performance.
486
+
487
+ **`twStyle(className: string)`**
488
+
489
+ ```typescript
490
+ function twStyle(className: string): TwStyle | undefined;
491
+ ```
492
+
493
+ String version for cases where template literals aren't needed. Returns `undefined` for empty strings.
494
+
495
+ **`setConfig(config: RuntimeConfig)`**
496
+
497
+ ```typescript
498
+ function setConfig(config: RuntimeConfig): void;
499
+ ```
500
+
501
+ Configure runtime theme settings (colors, etc.). Matches `tailwind.config.mjs` structure.
502
+
503
+ **`clearCache()`**
504
+
505
+ ```typescript
506
+ function clearCache(): void;
507
+ ```
508
+
509
+ Clears the internal memoization cache. Useful for testing.
510
+
511
+ **`getCacheStats()`**
512
+
513
+ ```typescript
514
+ function getCacheStats(): { size: number; keys: string[] };
515
+ ```
516
+
517
+ Returns cache statistics for debugging/monitoring.
518
+
519
+ #### Performance Considerations
520
+
521
+ - **Bundle Size**: The runtime module adds ~25KB minified (~15-20KB gzipped) to your bundle
522
+ - **Caching**: All parsed styles are automatically memoized, so repeated className strings have minimal overhead
523
+ - **When to Use**:
524
+ - ✅ Truly dynamic values that can't be determined at compile-time
525
+ - ✅ Prototyping and rapid development
526
+ - ❌ Static styles (use compile-time `className` instead for zero overhead)
527
+ - ❌ Performance-critical hot paths (compile-time is faster)
528
+
529
+ #### Runtime vs Compile-Time
530
+
531
+ | Feature | Compile-Time (`className`) | Runtime (`tw` tag) |
532
+ | -------------- | ------------------------------- | ----------------------- |
533
+ | Bundle Size | Only used styles (~4KB typical) | Full parser (~25KB) |
534
+ | Performance | Zero overhead (pre-compiled) | Fast (memoized parsing) |
535
+ | Dynamic Values | Conditional only | Fully dynamic |
536
+ | Custom Colors | Via `tailwind.config.*` | Via `setConfig()` |
537
+ | Type Safety | Full TypeScript support | Full TypeScript support |
538
+
539
+ **Recommendation**: Use compile-time `className` by default for best performance. Use runtime `tw` only when you need fully dynamic styling that can't be expressed with conditional logic.
540
+
281
541
  ### State Modifiers
282
542
 
283
543
  Apply styles based on component state with zero runtime overhead. The Babel plugin automatically generates optimized style functions.
@@ -977,6 +1237,173 @@ This limitation exists because the current parser architecture uses `Object.assi
977
1237
 
978
1238
  ## Advanced
979
1239
 
1240
+ ### Custom Attributes
1241
+
1242
+ By default, the Babel plugin transforms these className-like attributes to their corresponding style props:
1243
+
1244
+ - `className` → `style`
1245
+ - `contentContainerClassName` → `contentContainerStyle` (ScrollView, FlatList)
1246
+ - `columnWrapperClassName` → `columnWrapperStyle` (FlatList)
1247
+ - `ListHeaderComponentClassName` → `ListHeaderComponentStyle` (FlatList)
1248
+ - `ListFooterComponentClassName` → `ListFooterComponentStyle` (FlatList)
1249
+
1250
+ You can customize which attributes are transformed using the `attributes` plugin option:
1251
+
1252
+ **Exact Matches:**
1253
+
1254
+ ```javascript
1255
+ // babel.config.js
1256
+ module.exports = {
1257
+ plugins: [
1258
+ [
1259
+ "@mgcrea/react-native-tailwind/babel",
1260
+ {
1261
+ attributes: ["className", "buttonClassName", "containerClassName"],
1262
+ },
1263
+ ],
1264
+ ],
1265
+ };
1266
+ ```
1267
+
1268
+ **Pattern Matching:**
1269
+
1270
+ Use glob patterns to match multiple attributes:
1271
+
1272
+ ```javascript
1273
+ // babel.config.js
1274
+ module.exports = {
1275
+ plugins: [
1276
+ [
1277
+ "@mgcrea/react-native-tailwind/babel",
1278
+ {
1279
+ // Matches any attribute ending in 'ClassName'
1280
+ attributes: ["*ClassName"],
1281
+ },
1282
+ ],
1283
+ ],
1284
+ };
1285
+ ```
1286
+
1287
+ **Combined:**
1288
+
1289
+ ```javascript
1290
+ // babel.config.js
1291
+ module.exports = {
1292
+ plugins: [
1293
+ [
1294
+ "@mgcrea/react-native-tailwind/babel",
1295
+ {
1296
+ // Mix exact matches and patterns
1297
+ attributes: [
1298
+ "className",
1299
+ "*ClassName", // containerClassName, buttonClassName, etc.
1300
+ "custom*", // customButton, customHeader, etc.
1301
+ ],
1302
+ },
1303
+ ],
1304
+ ],
1305
+ };
1306
+ ```
1307
+
1308
+ **Usage Example:**
1309
+
1310
+ ```tsx
1311
+ // With custom attributes configured
1312
+ function Button({ title, onPress, buttonClassName, containerClassName }) {
1313
+ return (
1314
+ <View containerClassName="p-2 bg-gray-100">
1315
+ <Pressable buttonClassName="bg-blue-500 px-6 py-4 rounded-lg" onPress={onPress}>
1316
+ <Text className="text-white font-semibold">{title}</Text>
1317
+ </Pressable>
1318
+ </View>
1319
+ );
1320
+ }
1321
+
1322
+ // Transforms to:
1323
+ function Button({ title, onPress, buttonStyle, containerStyle }) {
1324
+ return (
1325
+ <View style={[_twStyles._bg_gray_100_p_2, containerStyle]}>
1326
+ <Pressable style={[_twStyles._bg_blue_500_px_6_py_4_rounded_lg, buttonStyle]} onPress={onPress}>
1327
+ <Text style={_twStyles._font_semibold_text_white}>{title}</Text>
1328
+ </Pressable>
1329
+ </View>
1330
+ );
1331
+ }
1332
+ ```
1333
+
1334
+ **Naming Convention:**
1335
+
1336
+ Attributes ending in `ClassName` are automatically converted to their `Style` equivalent:
1337
+
1338
+ - `buttonClassName` → `buttonStyle`
1339
+ - `containerClassName` → `containerStyle`
1340
+ - `headerClassName` → `headerStyle`
1341
+
1342
+ For attributes not ending in `ClassName`, the `style` prop is used.
1343
+
1344
+ **TypeScript Support:**
1345
+
1346
+ When using custom attributes, you'll need to augment the component types to include your custom className props. See the [TypeScript section](#2-enable-typescript-support-typescript) for details on module augmentation.
1347
+
1348
+ ### Custom Styles Identifier
1349
+
1350
+ By default, the Babel plugin generates a StyleSheet constant named `_twStyles`. You can customize this identifier to avoid conflicts or match your project's naming conventions:
1351
+
1352
+ ```javascript
1353
+ // babel.config.js
1354
+ module.exports = {
1355
+ plugins: [
1356
+ [
1357
+ "@mgcrea/react-native-tailwind/babel",
1358
+ {
1359
+ stylesIdentifier: "styles", // or 'tw', 'tailwind', etc.
1360
+ },
1361
+ ],
1362
+ ],
1363
+ };
1364
+ ```
1365
+
1366
+ **Default behavior:**
1367
+
1368
+ ```tsx
1369
+ // Input
1370
+ <View className="p-4 bg-blue-500" />
1371
+
1372
+ // Output
1373
+ <View style={_twStyles._bg_blue_500_p_4} />
1374
+
1375
+ const _twStyles = StyleSheet.create({
1376
+ _bg_blue_500_p_4: { padding: 16, backgroundColor: '#3B82F6' }
1377
+ });
1378
+ ```
1379
+
1380
+ **With custom identifier:**
1381
+
1382
+ ```tsx
1383
+ // Input (with stylesIdentifier: "styles")
1384
+ <View className="p-4 bg-blue-500" />
1385
+
1386
+ // Output
1387
+ <View style={styles._bg_blue_500_p_4} />
1388
+
1389
+ const styles = StyleSheet.create({
1390
+ _bg_blue_500_p_4: { padding: 16, backgroundColor: '#3B82F6' }
1391
+ });
1392
+ ```
1393
+
1394
+ **Use Cases:**
1395
+
1396
+ - **Avoid conflicts:** If you already have a `_twStyles` variable in your code
1397
+ - **Consistency:** Match your existing StyleSheet naming convention (`styles`, `styleSheet`, etc.)
1398
+ - **Shorter names:** Use a shorter identifier like `tw` or `s` for more compact code
1399
+ - **Team conventions:** Align with your team's coding standards
1400
+
1401
+ **Important Notes:**
1402
+
1403
+ - The identifier must be a valid JavaScript variable name
1404
+ - Choose a name that won't conflict with existing variables in your files
1405
+ - The same identifier is used across all files in your project
1406
+
980
1407
  ### Arbitrary Values
981
1408
 
982
1409
  Use arbitrary values for custom sizes, spacing, and borders not in the preset scales:
@@ -7,6 +7,7 @@
7
7
 
8
8
  import * as fs from "fs";
9
9
  import * as path from "path";
10
+ import { flattenColors } from "../utils/flattenColors";
10
11
 
11
12
  export type TailwindConfig = {
12
13
  theme?: {
@@ -79,29 +80,6 @@ export function loadTailwindConfig(configPath: string): TailwindConfig | null {
79
80
  }
80
81
  }
81
82
 
82
- /**
83
- * Flatten nested color objects into dot notation
84
- * Example: { brand: { light: '#fff', dark: '#000' } } -> { 'brand-light': '#fff', 'brand-dark': '#000' }
85
- */
86
- function flattenColors(
87
- colors: Record<string, string | Record<string, string>>,
88
- prefix = "",
89
- ): Record<string, string> {
90
- const result: Record<string, string> = {};
91
-
92
- for (const [key, value] of Object.entries(colors)) {
93
- const newKey = prefix ? `${prefix}-${key}` : key;
94
-
95
- if (typeof value === "string") {
96
- result[newKey] = value;
97
- } else if (typeof value === "object" && value !== null) {
98
- Object.assign(result, flattenColors(value, newKey));
99
- }
100
- }
101
-
102
- return result;
103
- }
104
-
105
83
  /**
106
84
  * Extract custom colors from tailwind config
107
85
  * Prefers theme.extend.colors over theme.colors to avoid overriding defaults