@mgcrea/react-native-tailwind 0.10.0 → 0.11.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.
Files changed (45) hide show
  1. package/README.md +159 -13
  2. package/dist/babel/config-loader.d.ts +12 -3
  3. package/dist/babel/config-loader.test.ts +14 -12
  4. package/dist/babel/config-loader.ts +41 -9
  5. package/dist/babel/index.cjs +91 -54
  6. package/dist/babel/plugin.d.ts +39 -1
  7. package/dist/babel/plugin.test.ts +275 -1
  8. package/dist/babel/plugin.ts +84 -25
  9. package/dist/babel/utils/colorSchemeModifierProcessing.d.ts +3 -3
  10. package/dist/babel/utils/colorSchemeModifierProcessing.ts +4 -4
  11. package/dist/babel/utils/dynamicProcessing.d.ts +5 -5
  12. package/dist/babel/utils/dynamicProcessing.ts +11 -11
  13. package/dist/babel/utils/modifierProcessing.d.ts +3 -3
  14. package/dist/babel/utils/modifierProcessing.ts +5 -5
  15. package/dist/babel/utils/platformModifierProcessing.d.ts +3 -3
  16. package/dist/babel/utils/platformModifierProcessing.ts +4 -4
  17. package/dist/babel/utils/styleInjection.d.ts +5 -3
  18. package/dist/babel/utils/styleInjection.ts +38 -23
  19. package/dist/babel/utils/twProcessing.d.ts +3 -3
  20. package/dist/babel/utils/twProcessing.ts +6 -6
  21. package/dist/parser/index.d.ts +11 -4
  22. package/dist/parser/index.js +1 -1
  23. package/dist/parser/typography.d.ts +3 -1
  24. package/dist/parser/typography.js +1 -1
  25. package/dist/runtime.cjs +1 -1
  26. package/dist/runtime.cjs.map +3 -3
  27. package/dist/runtime.d.ts +8 -1
  28. package/dist/runtime.js +1 -1
  29. package/dist/runtime.js.map +3 -3
  30. package/dist/runtime.test.js +1 -1
  31. package/package.json +1 -1
  32. package/src/babel/config-loader.test.ts +14 -12
  33. package/src/babel/config-loader.ts +41 -9
  34. package/src/babel/plugin.test.ts +275 -1
  35. package/src/babel/plugin.ts +84 -25
  36. package/src/babel/utils/colorSchemeModifierProcessing.ts +4 -4
  37. package/src/babel/utils/dynamicProcessing.ts +11 -11
  38. package/src/babel/utils/modifierProcessing.ts +5 -5
  39. package/src/babel/utils/platformModifierProcessing.ts +4 -4
  40. package/src/babel/utils/styleInjection.ts +38 -23
  41. package/src/babel/utils/twProcessing.ts +6 -6
  42. package/src/parser/index.ts +16 -8
  43. package/src/parser/typography.ts +14 -2
  44. package/src/runtime.test.ts +7 -7
  45. package/src/runtime.ts +37 -14
package/README.md CHANGED
@@ -425,7 +425,7 @@ export function RuntimeExample() {
425
425
 
426
426
  #### Configuration
427
427
 
428
- Configure custom colors and other theme options using `setConfig()`:
428
+ Configure custom colors and font families using `setConfig()`:
429
429
 
430
430
  ```typescript
431
431
  import { setConfig } from '@mgcrea/react-native-tailwind/runtime';
@@ -442,13 +442,17 @@ setConfig({
442
442
  dark: '#CC0000',
443
443
  },
444
444
  },
445
+ fontFamily: {
446
+ sans: ['"SF Pro Rounded"'],
447
+ custom: ['"My Custom Font"'],
448
+ },
445
449
  },
446
450
  },
447
451
  });
448
452
 
449
- // Now you can use custom colors
453
+ // Now you can use custom theme
450
454
  <View style={tw`bg-primary p-4`} />
451
- <Text style={tw`text-brand-light`}>Custom color</Text>
455
+ <Text style={tw`text-brand-light font-custom`}>Custom styling</Text>
452
456
  ```
453
457
 
454
458
  #### API Reference
@@ -1365,7 +1369,7 @@ This pattern allows you to build component libraries with optimized default styl
1365
1369
 
1366
1370
  **Available shades:** `50`, `100`, `200`, `300`, `400`, `500`, `600`, `700`, `800`, `900`
1367
1371
 
1368
- > **Note:** You can extend the color palette with custom colors via `tailwind.config.*` — see [Custom Colors](#custom-colors)
1372
+ > **Note:** You can extend the color palette with custom colors via `tailwind.config.*` — see [Custom Theme Extensions](#custom-theme-extensions)
1369
1373
 
1370
1374
  **Opacity Modifiers:**
1371
1375
 
@@ -1883,6 +1887,135 @@ const styles = StyleSheet.create({
1883
1887
  - Choose a name that won't conflict with existing variables in your files
1884
1888
  - The same identifier is used across all files in your project
1885
1889
 
1890
+ ### Custom Color Scheme Hook
1891
+
1892
+ By default, the plugin uses React Native's built-in `useColorScheme()` hook for `dark:` and `light:` modifiers. You can configure it to use a custom color scheme hook from theme providers like React Navigation, Expo, or your own implementation.
1893
+
1894
+ **Configuration:**
1895
+
1896
+ ```javascript
1897
+ // babel.config.js
1898
+ module.exports = {
1899
+ plugins: [
1900
+ [
1901
+ "@mgcrea/react-native-tailwind/babel",
1902
+ {
1903
+ colorScheme: {
1904
+ importFrom: "@/hooks/useColorScheme", // Module to import from
1905
+ importName: "useColorScheme", // Hook name to import
1906
+ },
1907
+ },
1908
+ ],
1909
+ ],
1910
+ };
1911
+ ```
1912
+
1913
+ **Use Cases:**
1914
+
1915
+ #### 1. Custom Theme Provider
1916
+
1917
+ Override system color scheme with user preferences from a store:
1918
+
1919
+ ```typescript
1920
+ // src/hooks/useColorScheme.ts
1921
+ import { useColorScheme as useSystemColorScheme } from "react-native";
1922
+ import { profileStore } from "@/stores/profileStore";
1923
+ import { type ColorSchemeName } from "react-native";
1924
+
1925
+ export const useColorScheme = (): ColorSchemeName => {
1926
+ const systemColorScheme = useSystemColorScheme();
1927
+ const userTheme = profileStore.theme; // 'dark' | 'light' | 'auto'
1928
+
1929
+ // Return user preference, or fall back to system if set to 'auto'
1930
+ return userTheme === 'auto' ? systemColorScheme : userTheme;
1931
+ };
1932
+ ```
1933
+
1934
+ ```javascript
1935
+ // babel.config.js
1936
+ {
1937
+ colorScheme: {
1938
+ importFrom: "@/hooks/useColorScheme",
1939
+ importName: "useColorScheme"
1940
+ }
1941
+ }
1942
+ ```
1943
+
1944
+ #### 2. React Navigation Theme
1945
+
1946
+ Integrate with React Navigation's theme system:
1947
+
1948
+ ```typescript
1949
+ // Wrap React Navigation's useTheme to return ColorSchemeName
1950
+ import { useTheme as useNavTheme } from "@react-navigation/native";
1951
+ import { type ColorSchemeName } from "react-native";
1952
+
1953
+ export const useColorScheme = (): ColorSchemeName => {
1954
+ const { dark } = useNavTheme();
1955
+ return dark ? "dark" : "light";
1956
+ };
1957
+ ```
1958
+
1959
+ #### 3. Expo Router Theme
1960
+
1961
+ Use Expo Router's theme hook:
1962
+
1963
+ ```javascript
1964
+ // babel.config.js
1965
+ {
1966
+ colorScheme: {
1967
+ importFrom: "expo-router",
1968
+ importName: "useColorScheme"
1969
+ }
1970
+ }
1971
+ ```
1972
+
1973
+ #### 4. Testing
1974
+
1975
+ Mock color scheme for tests:
1976
+
1977
+ ```typescript
1978
+ // test/mocks/useColorScheme.ts
1979
+ export const useColorScheme = () => "light"; // Or "dark" for dark mode tests
1980
+ ```
1981
+
1982
+ ```javascript
1983
+ // babel.config.js (test environment)
1984
+ {
1985
+ colorScheme: {
1986
+ importFrom: "@/test/mocks/useColorScheme",
1987
+ importName: "useColorScheme"
1988
+ }
1989
+ }
1990
+ ```
1991
+
1992
+ #### How it works
1993
+
1994
+ When you use `dark:` or `light:` modifiers:
1995
+
1996
+ ```tsx
1997
+ <View className="bg-white dark:bg-gray-900" />
1998
+ ```
1999
+
2000
+ The plugin will:
2001
+
2002
+ 1. Import your custom hook: `import { useColorScheme } from "@/hooks/useColorScheme"`
2003
+ 2. Inject it in components: `const _twColorScheme = useColorScheme();`
2004
+ 3. Generate conditionals: `_twColorScheme === "dark" && styles._dark_bg_gray_900`
2005
+
2006
+ #### Default behavior (no configuration)
2007
+
2008
+ Without custom configuration, the plugin uses React Native's built-in hook:
2009
+
2010
+ - Import: `import { useColorScheme } from "react-native"`
2011
+ - This works out of the box for basic system color scheme detection
2012
+
2013
+ #### Requirements
2014
+
2015
+ - Your custom hook must return `ColorSchemeName` (type from React Native: `"light" | "dark" | null | undefined`)
2016
+ - The hook must be compatible with React's rules of hooks (can only be called in function components)
2017
+ - Import merging works automatically if you already import from the same source
2018
+
1886
2019
  ### Arbitrary Values
1887
2020
 
1888
2021
  Use arbitrary values for custom sizes, spacing, and borders not in the preset scales:
@@ -1911,9 +2044,9 @@ Use arbitrary values for custom sizes, spacing, and borders not in the preset sc
1911
2044
 
1912
2045
  > **Note:** CSS units (`rem`, `em`, `vh`, `vw`) are not supported by React Native.
1913
2046
 
1914
- ### Custom Colors
2047
+ ### Custom Theme Extensions
1915
2048
 
1916
- Extend the default color palette via `tailwind.config.*` in your project root:
2049
+ Extend the default color palette and font families via `tailwind.config.*` in your project root:
1917
2050
 
1918
2051
  ```javascript
1919
2052
  // tailwind.config.mjs
@@ -1929,16 +2062,21 @@ export default {
1929
2062
  dark: "#0c4a6e",
1930
2063
  },
1931
2064
  },
2065
+ fontFamily: {
2066
+ sans: ['"SF Pro Rounded"'],
2067
+ custom: ['"My Custom Font"'],
2068
+ },
1932
2069
  },
1933
2070
  },
1934
2071
  };
1935
2072
  ```
1936
2073
 
1937
- Then use your custom colors:
2074
+ Then use your custom theme:
1938
2075
 
1939
2076
  ```tsx
1940
2077
  <View className="bg-primary p-4">
1941
- <Text className="text-brand">Custom branded text</Text>
2078
+ <Text className="text-brand font-custom">Custom branded text</Text>
2079
+ <Text className="font-sans">SF Pro Rounded text</Text>
1942
2080
  <View className="bg-brand-light rounded-lg" />
1943
2081
  </View>
1944
2082
  ```
@@ -1946,13 +2084,14 @@ Then use your custom colors:
1946
2084
  **How it works:**
1947
2085
 
1948
2086
  - Babel plugin discovers config by traversing up from source files
1949
- - Custom colors merged with defaults at build time (custom takes precedence)
1950
- - Nested objects flattened with dash notation: `brand.light` → `brand-light`
2087
+ - Custom theme merged with defaults at build time (custom takes precedence)
2088
+ - Nested color objects flattened with dash notation: `brand.light` → `brand-light`
2089
+ - Font families use first font in array (React Native doesn't support font stacks)
1951
2090
  - Zero runtime overhead — all loading happens during compilation
1952
2091
 
1953
2092
  **Supported formats:** `.js`, `.mjs`, `.cjs`, `.ts`
1954
2093
 
1955
- > **Tip:** Use `theme.extend.colors` to keep default Tailwind colors. Using `theme.colors` directly will override all defaults.
2094
+ > **Tip:** Use `theme.extend.*` to keep defaults. Using `theme.colors` or `theme.fontFamily` directly will override all defaults.
1956
2095
 
1957
2096
  ### Programmatic API
1958
2097
 
@@ -1961,10 +2100,17 @@ Access the parser and constants programmatically:
1961
2100
  ```typescript
1962
2101
  import { parseClassName, COLORS, SPACING_SCALE } from "@mgcrea/react-native-tailwind";
1963
2102
 
1964
- // Parse className strings
1965
- const _twStyles = parseClassName("m-4 p-2 bg-blue-500");
2103
+ // Parse className strings (no custom theme)
2104
+ const styles = parseClassName("m-4 p-2 bg-blue-500");
1966
2105
  // Returns: { margin: 16, padding: 8, backgroundColor: '#3B82F6' }
1967
2106
 
2107
+ // Parse with custom theme
2108
+ const customStyles = parseClassName("m-4 bg-primary font-custom", {
2109
+ colors: { primary: "#1d4ed8" },
2110
+ fontFamily: { custom: "My Custom Font" },
2111
+ });
2112
+ // Returns: { margin: 16, backgroundColor: '#1d4ed8', fontFamily: 'My Custom Font' }
2113
+
1968
2114
  // Access default scales
1969
2115
  const blueColor = COLORS["blue-500"]; // '#3B82F6'
1970
2116
  const spacing = SPACING_SCALE[4]; // 16
@@ -6,8 +6,10 @@ export type TailwindConfig = {
6
6
  theme?: {
7
7
  extend?: {
8
8
  colors?: Record<string, string | Record<string, string>>;
9
+ fontFamily?: Record<string, string | string[]>;
9
10
  };
10
11
  colors?: Record<string, string | Record<string, string>>;
12
+ fontFamily?: Record<string, string | string[]>;
11
13
  };
12
14
  };
13
15
  /**
@@ -19,7 +21,14 @@ export declare function findTailwindConfig(startDir: string): string | null;
19
21
  */
20
22
  export declare function loadTailwindConfig(configPath: string): TailwindConfig | null;
21
23
  /**
22
- * Extract custom colors from tailwind config
23
- * Prefers theme.extend.colors over theme.colors to avoid overriding defaults
24
+ * Custom theme configuration extracted from tailwind.config
24
25
  */
25
- export declare function extractCustomColors(filename: string): Record<string, string>;
26
+ export type CustomTheme = {
27
+ colors: Record<string, string>;
28
+ fontFamily: Record<string, string>;
29
+ };
30
+ /**
31
+ * Extract all custom theme extensions from tailwind config
32
+ * Prefers theme.extend.* over theme.* to avoid overriding defaults
33
+ */
34
+ export declare function extractCustomTheme(filename: string): CustomTheme;
@@ -1,6 +1,6 @@
1
1
  import * as fs from "fs";
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
- import { extractCustomColors, findTailwindConfig, loadTailwindConfig } from "./config-loader";
3
+ import { extractCustomTheme, findTailwindConfig, loadTailwindConfig } from "./config-loader";
4
4
 
5
5
  // Mock fs
6
6
  vi.mock("fs");
@@ -115,15 +115,15 @@ describe("config-loader", () => {
115
115
  });
116
116
  });
117
117
 
118
- describe("extractCustomColors", () => {
119
- it("should return empty object when no config found", () => {
118
+ describe("extractCustomTheme", () => {
119
+ it("should return empty theme when no config found", () => {
120
120
  vi.spyOn(fs, "existsSync").mockReturnValue(false);
121
121
 
122
- const result = extractCustomColors("/project/src/file.ts");
123
- expect(result).toEqual({});
122
+ const result = extractCustomTheme("/project/src/file.ts");
123
+ expect(result).toEqual({ colors: {}, fontFamily: {} });
124
124
  });
125
125
 
126
- it("should return empty object when config has no theme", () => {
126
+ it("should return empty theme when config has no theme", () => {
127
127
  const configPath = "/project/tailwind.config.js";
128
128
 
129
129
  vi.spyOn(fs, "existsSync").mockImplementation((filepath) => filepath === configPath);
@@ -131,22 +131,24 @@ describe("config-loader", () => {
131
131
 
132
132
  // loadTailwindConfig will be called, but we've already tested it
133
133
  // For integration, we'd need to mock the entire flow
134
- const result = extractCustomColors("/project/src/file.ts");
134
+ const result = extractCustomTheme("/project/src/file.ts");
135
135
 
136
136
  // Without actual config loading, this returns empty
137
- expect(result).toEqual({});
137
+ expect(result).toEqual({ colors: {}, fontFamily: {} });
138
138
  });
139
139
 
140
- it("should extract colors from theme.extend.colors", () => {
140
+ it("should extract colors and fontFamily from theme.extend", () => {
141
141
  // This would require complex mocking of the entire require flow
142
- // Testing the logic: theme.extend.colors is preferred
142
+ // Testing the logic: theme.extend is preferred
143
143
  const colors = { brand: { light: "#fff", dark: "#000" } };
144
+ const fontFamily = { sans: ['"SF Pro"'], custom: ['"Custom Font"'] };
144
145
  const theme = {
145
- extend: { colors },
146
+ extend: { colors, fontFamily },
146
147
  };
147
148
 
148
- // If we had the config, we'd flatten the colors
149
+ // If we had the config, we'd flatten the colors and convert fontFamily
149
150
  expect(theme.extend.colors).toEqual(colors);
151
+ expect(theme.extend.fontFamily).toEqual(fontFamily);
150
152
  });
151
153
  });
152
154
  });
@@ -13,8 +13,10 @@ export type TailwindConfig = {
13
13
  theme?: {
14
14
  extend?: {
15
15
  colors?: Record<string, string | Record<string, string>>;
16
+ fontFamily?: Record<string, string | string[]>;
16
17
  };
17
18
  colors?: Record<string, string | Record<string, string>>;
19
+ fontFamily?: Record<string, string | string[]>;
18
20
  };
19
21
  };
20
22
 
@@ -82,23 +84,31 @@ export function loadTailwindConfig(configPath: string): TailwindConfig | null {
82
84
  }
83
85
 
84
86
  /**
85
- * Extract custom colors from tailwind config
86
- * Prefers theme.extend.colors over theme.colors to avoid overriding defaults
87
+ * Custom theme configuration extracted from tailwind.config
87
88
  */
88
- export function extractCustomColors(filename: string): Record<string, string> {
89
+ export type CustomTheme = {
90
+ colors: Record<string, string>;
91
+ fontFamily: Record<string, string>;
92
+ };
93
+
94
+ /**
95
+ * Extract all custom theme extensions from tailwind config
96
+ * Prefers theme.extend.* over theme.* to avoid overriding defaults
97
+ */
98
+ export function extractCustomTheme(filename: string): CustomTheme {
89
99
  const projectDir = path.dirname(filename);
90
100
  const configPath = findTailwindConfig(projectDir);
91
101
 
92
102
  if (!configPath) {
93
- return {};
103
+ return { colors: {}, fontFamily: {} };
94
104
  }
95
105
 
96
106
  const config = loadTailwindConfig(configPath);
97
107
  if (!config?.theme) {
98
- return {};
108
+ return { colors: {}, fontFamily: {} };
99
109
  }
100
110
 
101
- // Warn if using theme.colors instead of theme.extend.colors
111
+ // Extract colors
102
112
  /* v8 ignore next 5 */
103
113
  if (config.theme.colors && !config.theme.extend?.colors && process.env.NODE_ENV !== "production") {
104
114
  console.warn(
@@ -106,9 +116,31 @@ export function extractCustomColors(filename: string): Record<string, string> {
106
116
  "Use theme.extend.colors to add custom colors while keeping defaults.",
107
117
  );
108
118
  }
109
-
110
- // Prefer theme.extend.colors
111
119
  const colors = config.theme.extend?.colors ?? config.theme.colors ?? {};
112
120
 
113
- return flattenColors(colors);
121
+ // Extract fontFamily
122
+ /* v8 ignore next 5 */
123
+ if (config.theme.fontFamily && !config.theme.extend?.fontFamily && process.env.NODE_ENV !== "production") {
124
+ console.warn(
125
+ "[react-native-tailwind] Using theme.fontFamily will override all default font families. " +
126
+ "Use theme.extend.fontFamily to add custom fonts while keeping defaults.",
127
+ );
128
+ }
129
+ const fontFamily = config.theme.extend?.fontFamily ?? config.theme.fontFamily ?? {};
130
+
131
+ // Convert fontFamily values to strings (take first value if array)
132
+ const fontFamilyResult: Record<string, string> = {};
133
+ for (const [key, value] of Object.entries(fontFamily)) {
134
+ if (Array.isArray(value)) {
135
+ // Take first font in the array (React Native doesn't support font stacks)
136
+ fontFamilyResult[key] = value[0];
137
+ } else {
138
+ fontFamilyResult[key] = value;
139
+ }
140
+ }
141
+
142
+ return {
143
+ colors: flattenColors(colors),
144
+ fontFamily: fontFamilyResult,
145
+ };
114
146
  }