@mgcrea/react-native-tailwind 0.9.1 → 0.11.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 (55) hide show
  1. package/README.md +386 -43
  2. package/dist/babel/config-loader.d.ts +12 -3
  3. package/dist/babel/config-loader.test.ts +154 -0
  4. package/dist/babel/config-loader.ts +41 -9
  5. package/dist/babel/index.cjs +592 -69
  6. package/dist/babel/plugin.d.ts +23 -1
  7. package/dist/babel/plugin.test.ts +331 -0
  8. package/dist/babel/plugin.ts +268 -37
  9. package/dist/babel/utils/colorSchemeModifierProcessing.d.ts +34 -0
  10. package/dist/babel/utils/colorSchemeModifierProcessing.ts +89 -0
  11. package/dist/babel/utils/dynamicProcessing.d.ts +34 -3
  12. package/dist/babel/utils/dynamicProcessing.ts +358 -39
  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 +13 -0
  18. package/dist/babel/utils/styleInjection.ts +101 -0
  19. package/dist/babel/utils/styleTransforms.test.ts +56 -0
  20. package/dist/babel/utils/twProcessing.d.ts +5 -3
  21. package/dist/babel/utils/twProcessing.ts +27 -6
  22. package/dist/parser/index.d.ts +13 -6
  23. package/dist/parser/index.js +1 -1
  24. package/dist/parser/modifiers.d.ts +48 -2
  25. package/dist/parser/modifiers.js +1 -1
  26. package/dist/parser/modifiers.test.js +1 -1
  27. package/dist/parser/typography.d.ts +3 -1
  28. package/dist/parser/typography.js +1 -1
  29. package/dist/runtime.cjs +1 -1
  30. package/dist/runtime.cjs.map +3 -3
  31. package/dist/runtime.d.ts +8 -1
  32. package/dist/runtime.js +1 -1
  33. package/dist/runtime.js.map +3 -3
  34. package/dist/runtime.test.js +1 -1
  35. package/dist/types/config.d.ts +7 -0
  36. package/dist/types/config.js +0 -0
  37. package/package.json +3 -2
  38. package/src/babel/config-loader.test.ts +154 -0
  39. package/src/babel/config-loader.ts +41 -9
  40. package/src/babel/plugin.test.ts +331 -0
  41. package/src/babel/plugin.ts +268 -37
  42. package/src/babel/utils/colorSchemeModifierProcessing.ts +89 -0
  43. package/src/babel/utils/dynamicProcessing.ts +358 -39
  44. package/src/babel/utils/modifierProcessing.ts +5 -5
  45. package/src/babel/utils/platformModifierProcessing.ts +4 -4
  46. package/src/babel/utils/styleInjection.ts +101 -0
  47. package/src/babel/utils/styleTransforms.test.ts +56 -0
  48. package/src/babel/utils/twProcessing.ts +27 -6
  49. package/src/parser/index.ts +28 -9
  50. package/src/parser/modifiers.test.ts +151 -1
  51. package/src/parser/modifiers.ts +139 -4
  52. package/src/parser/typography.ts +14 -2
  53. package/src/runtime.test.ts +7 -7
  54. package/src/runtime.ts +37 -14
  55. package/src/types/config.ts +7 -0
@@ -64,6 +64,107 @@ export function addPlatformImport(path: NodePath<BabelTypes.Program>, t: typeof
64
64
  }
65
65
  }
66
66
 
67
+ /**
68
+ * Add useColorScheme import to the file or merge with existing react-native import
69
+ */
70
+ export function addColorSchemeImport(path: NodePath<BabelTypes.Program>, t: typeof BabelTypes): void {
71
+ // Check if there's already a react-native import
72
+ const body = path.node.body;
73
+ let reactNativeImport: BabelTypes.ImportDeclaration | null = null;
74
+
75
+ for (const statement of body) {
76
+ if (t.isImportDeclaration(statement) && statement.source.value === "react-native") {
77
+ reactNativeImport = statement;
78
+ break;
79
+ }
80
+ }
81
+
82
+ if (reactNativeImport) {
83
+ // Check if useColorScheme is already imported
84
+ const hasUseColorScheme = reactNativeImport.specifiers.some(
85
+ (spec) =>
86
+ t.isImportSpecifier(spec) &&
87
+ spec.imported.type === "Identifier" &&
88
+ spec.imported.name === "useColorScheme",
89
+ );
90
+
91
+ if (!hasUseColorScheme) {
92
+ // Add useColorScheme to existing react-native import
93
+ reactNativeImport.specifiers.push(
94
+ t.importSpecifier(t.identifier("useColorScheme"), t.identifier("useColorScheme")),
95
+ );
96
+ }
97
+ } else {
98
+ // Create new react-native import with useColorScheme
99
+ const importDeclaration = t.importDeclaration(
100
+ [t.importSpecifier(t.identifier("useColorScheme"), t.identifier("useColorScheme"))],
101
+ t.stringLiteral("react-native"),
102
+ );
103
+ path.unshiftContainer("body", importDeclaration);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Inject useColorScheme hook call at the top of a function component
109
+ *
110
+ * @param functionPath - Path to the function component
111
+ * @param colorSchemeVariableName - Name for the color scheme variable
112
+ * @param t - Babel types
113
+ * @returns true if hook was injected, false if already exists
114
+ */
115
+ export function injectColorSchemeHook(
116
+ functionPath: NodePath<BabelTypes.Function>,
117
+ colorSchemeVariableName: string,
118
+ t: typeof BabelTypes,
119
+ ): boolean {
120
+ let body = functionPath.node.body;
121
+
122
+ // Handle concise arrow functions: () => <JSX />
123
+ // Convert to block statement: () => { const _twColorScheme = useColorScheme(); return <JSX />; }
124
+ if (!t.isBlockStatement(body)) {
125
+ if (t.isArrowFunctionExpression(functionPath.node) && t.isExpression(body)) {
126
+ // Convert concise body to block statement with return
127
+ const returnStatement = t.returnStatement(body);
128
+ const blockStatement = t.blockStatement([returnStatement]);
129
+ functionPath.node.body = blockStatement;
130
+ body = blockStatement;
131
+ } else {
132
+ // Other non-block functions (shouldn't happen for components, but be safe)
133
+ return false;
134
+ }
135
+ }
136
+
137
+ // Check if hook is already injected
138
+ const hasHook = body.body.some((statement) => {
139
+ if (
140
+ t.isVariableDeclaration(statement) &&
141
+ statement.declarations.length > 0 &&
142
+ t.isVariableDeclarator(statement.declarations[0])
143
+ ) {
144
+ const declarator = statement.declarations[0];
145
+ return t.isIdentifier(declarator.id) && declarator.id.name === colorSchemeVariableName;
146
+ }
147
+ return false;
148
+ });
149
+
150
+ if (hasHook) {
151
+ return false; // Already injected
152
+ }
153
+
154
+ // Create: const _twColorScheme = useColorScheme();
155
+ const hookCall = t.variableDeclaration("const", [
156
+ t.variableDeclarator(
157
+ t.identifier(colorSchemeVariableName),
158
+ t.callExpression(t.identifier("useColorScheme"), []),
159
+ ),
160
+ ]);
161
+
162
+ // Insert at the beginning of function body
163
+ body.body.unshift(hookCall);
164
+
165
+ return true;
166
+ }
167
+
67
168
  /**
68
169
  * Inject StyleSheet.create with all collected styles at the top of the file
69
170
  * This ensures the styles object is defined before any code that references it
@@ -347,3 +347,59 @@ describe("Style merging - edge cases", () => {
347
347
  expect(output).not.toContain("className");
348
348
  });
349
349
  });
350
+
351
+ describe("Style function merging - mergeStyleFunctionAttribute", () => {
352
+ it("should merge modifier className with existing function style prop", () => {
353
+ const input = `
354
+ import { Pressable } from 'react-native';
355
+ export function Component() {
356
+ return (
357
+ <Pressable
358
+ className="bg-blue-500 active:bg-blue-700"
359
+ style={({ pressed }) => pressed && { opacity: 0.8 }}
360
+ />
361
+ );
362
+ }
363
+ `;
364
+
365
+ const output = transform(input);
366
+
367
+ // Should create wrapper function that merges both style functions
368
+ expect(output).toContain("_state");
369
+ expect(output).toMatch(/_state\s*=>/);
370
+
371
+ // Should call both the new style function and existing style function
372
+ expect(output).toContain("_bg_blue_500");
373
+ expect(output).toContain("_active_bg_blue_700");
374
+
375
+ // Should not have className in output
376
+ expect(output).not.toContain("className");
377
+ });
378
+
379
+ it("should merge modifier className with static existing style when using Pressable", () => {
380
+ const input = `
381
+ import { Pressable } from 'react-native';
382
+ export function Component() {
383
+ return (
384
+ <Pressable
385
+ className="p-4 active:bg-gray-100"
386
+ style={{ borderRadius: 8 }}
387
+ />
388
+ );
389
+ }
390
+ `;
391
+
392
+ const output = transform(input);
393
+
394
+ // Should create function that wraps className styles
395
+ expect(output).toContain("_state");
396
+
397
+ // Should include both the className styles and the static style
398
+ expect(output).toContain("_p_4");
399
+ expect(output).toContain("_active_bg_gray_100");
400
+ expect(output).toContain("borderRadius");
401
+
402
+ // Should not have className in output
403
+ expect(output).not.toContain("className");
404
+ });
405
+ });
@@ -4,7 +4,9 @@
4
4
 
5
5
  import type { NodePath } from "@babel/core";
6
6
  import type * as BabelTypes from "@babel/types";
7
- import type { ModifierType, ParsedModifier } from "../../parser/index.js";
7
+ import type { CustomTheme, ModifierType, ParsedModifier } from "../../parser/index.js";
8
+ import { expandSchemeModifier, isSchemeModifier } from "../../parser/index.js";
9
+ import type { SchemeModifierConfig } from "../../types/config.js";
8
10
  import type { StyleObject } from "../../types/core.js";
9
11
 
10
12
  /**
@@ -13,7 +15,8 @@ import type { StyleObject } from "../../types/core.js";
13
15
  // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
14
16
  export interface TwProcessingState {
15
17
  styleRegistry: Map<string, StyleObject>;
16
- customColors: Record<string, string>;
18
+ customTheme: CustomTheme;
19
+ schemeModifierConfig: SchemeModifierConfig;
17
20
  stylesIdentifier: string;
18
21
  }
19
22
 
@@ -25,12 +28,30 @@ export function processTwCall(
25
28
  className: string,
26
29
  path: NodePath,
27
30
  state: TwProcessingState,
28
- parseClassName: (className: string, customColors: Record<string, string>) => StyleObject,
31
+ parseClassName: (className: string, customTheme?: CustomTheme) => StyleObject,
29
32
  generateStyleKey: (className: string) => string,
30
33
  splitModifierClasses: (className: string) => { baseClasses: string[]; modifierClasses: ParsedModifier[] },
31
34
  t: typeof BabelTypes,
32
35
  ): void {
33
- const { baseClasses, modifierClasses } = splitModifierClasses(className);
36
+ const { baseClasses, modifierClasses: rawModifierClasses } = splitModifierClasses(className);
37
+
38
+ // Expand scheme: modifiers into dark: and light: modifiers
39
+ const modifierClasses: ParsedModifier[] = [];
40
+ for (const modifier of rawModifierClasses) {
41
+ if (isSchemeModifier(modifier.modifier)) {
42
+ // Expand scheme: into dark: and light:
43
+ const expanded = expandSchemeModifier(
44
+ modifier,
45
+ state.customTheme.colors ?? {},
46
+ state.schemeModifierConfig.darkSuffix ?? "-dark",
47
+ state.schemeModifierConfig.lightSuffix ?? "-light",
48
+ );
49
+ modifierClasses.push(...expanded);
50
+ } else {
51
+ // Keep other modifiers as-is
52
+ modifierClasses.push(modifier);
53
+ }
54
+ }
34
55
 
35
56
  // Build TwStyle object properties
36
57
  const objectProperties: BabelTypes.ObjectProperty[] = [];
@@ -38,7 +59,7 @@ export function processTwCall(
38
59
  // Parse and add base styles
39
60
  if (baseClasses.length > 0) {
40
61
  const baseClassName = baseClasses.join(" ");
41
- const baseStyleObject = parseClassName(baseClassName, state.customColors);
62
+ const baseStyleObject = parseClassName(baseClassName, state.customTheme);
42
63
  const baseStyleKey = generateStyleKey(baseClassName);
43
64
  state.styleRegistry.set(baseStyleKey, baseStyleObject);
44
65
 
@@ -68,7 +89,7 @@ export function processTwCall(
68
89
  // Add modifier styles
69
90
  for (const [modifierType, modifiers] of modifiersByType) {
70
91
  const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
71
- const modifierStyleObject = parseClassName(modifierClassNames, state.customColors);
92
+ const modifierStyleObject = parseClassName(modifierClassNames, state.customTheme);
72
93
  const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
73
94
  state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
74
95
 
@@ -14,18 +14,26 @@ import { parseSpacing } from "./spacing";
14
14
  import { parseTransform } from "./transforms";
15
15
  import { parseTypography } from "./typography";
16
16
 
17
+ /**
18
+ * Custom theme configuration (subset of tailwind.config theme extensions)
19
+ */
20
+ export type CustomTheme = {
21
+ colors?: Record<string, string>;
22
+ fontFamily?: Record<string, string>;
23
+ };
24
+
17
25
  /**
18
26
  * Parse a className string and return a React Native style object
19
27
  * @param className - Space-separated class names
20
- * @param customColors - Optional custom colors from tailwind.config
28
+ * @param customTheme - Optional custom theme from tailwind.config
21
29
  * @returns React Native style object
22
30
  */
23
- export function parseClassName(className: string, customColors?: Record<string, string>): StyleObject {
31
+ export function parseClassName(className: string, customTheme?: CustomTheme): StyleObject {
24
32
  const classes = className.split(/\s+/).filter(Boolean);
25
33
  const style: StyleObject = {};
26
34
 
27
35
  for (const cls of classes) {
28
- const parsedStyle = parseClass(cls, customColors);
36
+ const parsedStyle = parseClass(cls, customTheme);
29
37
  Object.assign(style, parsedStyle);
30
38
  }
31
39
 
@@ -35,19 +43,19 @@ export function parseClassName(className: string, customColors?: Record<string,
35
43
  /**
36
44
  * Parse a single class name
37
45
  * @param cls - Single class name
38
- * @param customColors - Optional custom colors from tailwind.config
46
+ * @param customTheme - Optional custom theme from tailwind.config
39
47
  * @returns React Native style object
40
48
  */
41
- export function parseClass(cls: string, customColors?: Record<string, string>): StyleObject {
49
+ export function parseClass(cls: string, customTheme?: CustomTheme): StyleObject {
42
50
  // Try each parser in order
43
51
  // Note: parseBorder must come before parseColor to avoid border-[3px] being parsed as a color
44
- // parseColor gets custom colors, others don't need it
52
+ // parseColor and parseTypography get custom theme, others don't need it
45
53
  const parsers: Array<(cls: string) => StyleObject | null> = [
46
54
  parseSpacing,
47
55
  parseBorder,
48
- (cls: string) => parseColor(cls, customColors),
56
+ (cls: string) => parseColor(cls, customTheme?.colors),
49
57
  parseLayout,
50
- parseTypography,
58
+ (cls: string) => parseTypography(cls, customTheme?.fontFamily),
51
59
  parseSizing,
52
60
  parseShadow,
53
61
  parseAspectRatio,
@@ -84,10 +92,21 @@ export { parseTypography } from "./typography";
84
92
 
85
93
  // Re-export modifier utilities
86
94
  export {
95
+ expandSchemeModifier,
87
96
  hasModifier,
97
+ isColorClass,
98
+ isColorSchemeModifier,
88
99
  isPlatformModifier,
100
+ isSchemeModifier,
89
101
  isStateModifier,
90
102
  parseModifier,
91
103
  splitModifierClasses,
92
104
  } from "./modifiers";
93
- export type { ModifierType, ParsedModifier, PlatformModifierType, StateModifierType } from "./modifiers";
105
+ export type {
106
+ ColorSchemeModifierType,
107
+ ModifierType,
108
+ ParsedModifier,
109
+ PlatformModifierType,
110
+ SchemeModifierType,
111
+ StateModifierType,
112
+ } from "./modifiers";
@@ -1,6 +1,13 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import type { ModifierType, ParsedModifier } from "./modifiers";
3
- import { hasModifier, parseModifier, splitModifierClasses } from "./modifiers";
3
+ import {
4
+ expandSchemeModifier,
5
+ hasModifier,
6
+ isColorClass,
7
+ isSchemeModifier,
8
+ parseModifier,
9
+ splitModifierClasses,
10
+ } from "./modifiers";
4
11
 
5
12
  describe("parseModifier - basic functionality", () => {
6
13
  it("should parse active modifier", () => {
@@ -373,3 +380,146 @@ describe("type safety", () => {
373
380
  }
374
381
  });
375
382
  });
383
+
384
+ describe("isSchemeModifier", () => {
385
+ it("should return true for scheme modifier", () => {
386
+ expect(isSchemeModifier("scheme")).toBe(true);
387
+ });
388
+
389
+ it("should return false for non-scheme modifiers", () => {
390
+ expect(isSchemeModifier("dark")).toBe(false);
391
+ expect(isSchemeModifier("light")).toBe(false);
392
+ expect(isSchemeModifier("active")).toBe(false);
393
+ expect(isSchemeModifier("ios")).toBe(false);
394
+ });
395
+ });
396
+
397
+ describe("isColorClass", () => {
398
+ it("should return true for text color classes", () => {
399
+ expect(isColorClass("text-red-500")).toBe(true);
400
+ expect(isColorClass("text-systemGray")).toBe(true);
401
+ expect(isColorClass("text-blue-50")).toBe(true);
402
+ });
403
+
404
+ it("should return true for background color classes", () => {
405
+ expect(isColorClass("bg-red-500")).toBe(true);
406
+ expect(isColorClass("bg-systemGray")).toBe(true);
407
+ expect(isColorClass("bg-transparent")).toBe(true);
408
+ });
409
+
410
+ it("should return true for border color classes", () => {
411
+ expect(isColorClass("border-red-500")).toBe(true);
412
+ expect(isColorClass("border-systemGray")).toBe(true);
413
+ expect(isColorClass("border-black")).toBe(true);
414
+ });
415
+
416
+ it("should return false for non-color classes", () => {
417
+ expect(isColorClass("m-4")).toBe(false);
418
+ expect(isColorClass("p-2")).toBe(false);
419
+ expect(isColorClass("flex")).toBe(false);
420
+ expect(isColorClass("rounded-lg")).toBe(false);
421
+ expect(isColorClass("font-bold")).toBe(false);
422
+ });
423
+ });
424
+
425
+ describe("expandSchemeModifier", () => {
426
+ const customColors = {
427
+ "systemGray-dark": "#333333",
428
+ "systemGray-light": "#CCCCCC",
429
+ "primary-dark": "#1E40AF",
430
+ "primary-light": "#BFDBFE",
431
+ "accent-dark": "#DC2626",
432
+ "accent-light": "#FECACA",
433
+ };
434
+
435
+ it("should expand text color scheme modifier with default suffixes", () => {
436
+ const modifier = { modifier: "scheme" as const, baseClass: "text-systemGray" };
437
+ const result = expandSchemeModifier(modifier, customColors);
438
+
439
+ expect(result).toHaveLength(2);
440
+ expect((result as [ParsedModifier, ParsedModifier])[0]).toEqual({
441
+ modifier: "dark",
442
+ baseClass: "text-systemGray-dark",
443
+ });
444
+ expect((result as [ParsedModifier, ParsedModifier])[1]).toEqual({
445
+ modifier: "light",
446
+ baseClass: "text-systemGray-light",
447
+ });
448
+ });
449
+
450
+ it("should expand background color scheme modifier", () => {
451
+ const modifier = { modifier: "scheme" as const, baseClass: "bg-primary" };
452
+ const result = expandSchemeModifier(modifier, customColors);
453
+
454
+ expect(result).toHaveLength(2);
455
+ expect((result as [ParsedModifier, ParsedModifier])[0]).toEqual({
456
+ modifier: "dark",
457
+ baseClass: "bg-primary-dark",
458
+ });
459
+ expect((result as [ParsedModifier, ParsedModifier])[1]).toEqual({
460
+ modifier: "light",
461
+ baseClass: "bg-primary-light",
462
+ });
463
+ });
464
+
465
+ it("should expand border color scheme modifier", () => {
466
+ const modifier = { modifier: "scheme" as const, baseClass: "border-accent" };
467
+ const result = expandSchemeModifier(modifier, customColors);
468
+
469
+ expect(result).toHaveLength(2);
470
+ expect((result as [ParsedModifier, ParsedModifier])[0]).toEqual({
471
+ modifier: "dark",
472
+ baseClass: "border-accent-dark",
473
+ });
474
+ expect((result as [ParsedModifier, ParsedModifier])[1]).toEqual({
475
+ modifier: "light",
476
+ baseClass: "border-accent-light",
477
+ });
478
+ });
479
+
480
+ it("should use custom suffixes when provided", () => {
481
+ const modifier = { modifier: "scheme" as const, baseClass: "text-systemGray" };
482
+ const _result = expandSchemeModifier(modifier, customColors, "-darkMode", "-lightMode");
483
+
484
+ const expectedColors = {
485
+ "systemGray-darkMode": "#333333",
486
+ "systemGray-lightMode": "#CCCCCC",
487
+ };
488
+
489
+ expect(expandSchemeModifier(modifier, expectedColors, "-darkMode", "-lightMode")).toHaveLength(2);
490
+ });
491
+
492
+ it("should return empty array for non-color classes", () => {
493
+ const modifier = { modifier: "scheme" as const, baseClass: "m-4" };
494
+ const result = expandSchemeModifier(modifier, customColors);
495
+
496
+ expect(result).toEqual([]);
497
+ });
498
+
499
+ it("should return empty array when dark color variant is missing", () => {
500
+ const modifier = { modifier: "scheme" as const, baseClass: "text-missing" };
501
+ const incompleteColors = {
502
+ "missing-light": "#FFFFFF",
503
+ };
504
+ const result = expandSchemeModifier(modifier, incompleteColors);
505
+
506
+ expect(result).toEqual([]);
507
+ });
508
+
509
+ it("should return empty array when light color variant is missing", () => {
510
+ const modifier = { modifier: "scheme" as const, baseClass: "text-missing" };
511
+ const incompleteColors = {
512
+ "missing-dark": "#000000",
513
+ };
514
+ const result = expandSchemeModifier(modifier, incompleteColors);
515
+
516
+ expect(result).toEqual([]);
517
+ });
518
+
519
+ it("should return empty array when both color variants are missing", () => {
520
+ const modifier = { modifier: "scheme" as const, baseClass: "text-missing" };
521
+ const result = expandSchemeModifier(modifier, customColors);
522
+
523
+ expect(result).toEqual([]);
524
+ });
525
+ });
@@ -1,12 +1,19 @@
1
1
  /**
2
- * Modifier parsing utilities for state-based and platform-specific class names
2
+ * Modifier parsing utilities for state-based, platform-specific, and color scheme class names
3
3
  * - State modifiers: active:, hover:, focus:, disabled:, placeholder:
4
4
  * - Platform modifiers: ios:, android:, web:
5
+ * - Color scheme modifiers: dark:, light:
5
6
  */
6
7
 
7
8
  export type StateModifierType = "active" | "hover" | "focus" | "disabled" | "placeholder";
8
9
  export type PlatformModifierType = "ios" | "android" | "web";
9
- export type ModifierType = StateModifierType | PlatformModifierType;
10
+ export type ColorSchemeModifierType = "dark" | "light";
11
+ export type SchemeModifierType = "scheme";
12
+ export type ModifierType =
13
+ | StateModifierType
14
+ | PlatformModifierType
15
+ | ColorSchemeModifierType
16
+ | SchemeModifierType;
10
17
 
11
18
  export type ParsedModifier = {
12
19
  modifier: ModifierType;
@@ -30,9 +37,24 @@ const STATE_MODIFIERS: readonly StateModifierType[] = [
30
37
  const PLATFORM_MODIFIERS: readonly PlatformModifierType[] = ["ios", "android", "web"] as const;
31
38
 
32
39
  /**
33
- * All supported modifiers (state + platform)
40
+ * Supported color scheme modifiers that map to Appearance.colorScheme values
34
41
  */
35
- const SUPPORTED_MODIFIERS: readonly ModifierType[] = [...STATE_MODIFIERS, ...PLATFORM_MODIFIERS] as const;
42
+ const COLOR_SCHEME_MODIFIERS: readonly ColorSchemeModifierType[] = ["dark", "light"] as const;
43
+
44
+ /**
45
+ * Scheme modifier that expands to both dark: and light: modifiers
46
+ */
47
+ const SCHEME_MODIFIERS: readonly SchemeModifierType[] = ["scheme"] as const;
48
+
49
+ /**
50
+ * All supported modifiers (state + platform + color scheme + scheme)
51
+ */
52
+ const SUPPORTED_MODIFIERS: readonly ModifierType[] = [
53
+ ...STATE_MODIFIERS,
54
+ ...PLATFORM_MODIFIERS,
55
+ ...COLOR_SCHEME_MODIFIERS,
56
+ ...SCHEME_MODIFIERS,
57
+ ] as const;
36
58
 
37
59
  /**
38
60
  * Parse a class name to detect and extract modifiers
@@ -107,6 +129,119 @@ export function isPlatformModifier(modifier: ModifierType): modifier is Platform
107
129
  return PLATFORM_MODIFIERS.includes(modifier as PlatformModifierType);
108
130
  }
109
131
 
132
+ /**
133
+ * Check if a modifier is a color scheme modifier (dark, light)
134
+ *
135
+ * @param modifier - Modifier type to check
136
+ * @returns true if modifier is a color scheme modifier
137
+ */
138
+ export function isColorSchemeModifier(modifier: ModifierType): modifier is ColorSchemeModifierType {
139
+ return COLOR_SCHEME_MODIFIERS.includes(modifier as ColorSchemeModifierType);
140
+ }
141
+
142
+ /**
143
+ * Check if a modifier is a scheme modifier (scheme)
144
+ *
145
+ * @param modifier - Modifier type to check
146
+ * @returns true if modifier is a scheme modifier
147
+ */
148
+ export function isSchemeModifier(modifier: ModifierType): modifier is SchemeModifierType {
149
+ return SCHEME_MODIFIERS.includes(modifier as SchemeModifierType);
150
+ }
151
+
152
+ /**
153
+ * Check if a class name is a color-based utility class
154
+ *
155
+ * @param className - Class name to check
156
+ * @returns true if class is color-based (text-*, bg-*, border-*)
157
+ */
158
+ export function isColorClass(className: string): boolean {
159
+ return className.startsWith("text-") || className.startsWith("bg-") || className.startsWith("border-");
160
+ }
161
+
162
+ /**
163
+ * Expand scheme modifier into dark and light modifiers
164
+ *
165
+ * @param schemeModifier - Parsed scheme modifier
166
+ * @param customColors - Custom colors from config
167
+ * @param darkSuffix - Suffix for dark variant (default: "-dark")
168
+ * @param lightSuffix - Suffix for light variant (default: "-light")
169
+ * @returns Array of expanded modifiers (dark: and light:), or empty array if validation fails
170
+ *
171
+ * @example
172
+ * expandSchemeModifier(
173
+ * { modifier: "scheme", baseClass: "text-systemGray" },
174
+ * { "systemGray-dark": "#333", "systemGray-light": "#ccc" },
175
+ * "-dark",
176
+ * "-light"
177
+ * )
178
+ * // Returns: [
179
+ * // { modifier: "dark", baseClass: "text-systemGray-dark" },
180
+ * // { modifier: "light", baseClass: "text-systemGray-light" }
181
+ * // ]
182
+ */
183
+ export function expandSchemeModifier(
184
+ schemeModifier: ParsedModifier,
185
+ customColors: Record<string, string>,
186
+ darkSuffix = "-dark",
187
+ lightSuffix = "-light",
188
+ ): ParsedModifier[] {
189
+ const { baseClass } = schemeModifier;
190
+
191
+ // Only process color-based classes
192
+ if (!isColorClass(baseClass)) {
193
+ if (process.env.NODE_ENV !== "production") {
194
+ console.warn(
195
+ `[react-native-tailwind] scheme: modifier only supports color classes (text-*, bg-*, border-*). ` +
196
+ `Found: "${baseClass}". This modifier will be ignored.`,
197
+ );
198
+ }
199
+ return [];
200
+ }
201
+
202
+ // Extract the color name from the class
203
+ // e.g., "text-systemGray" -> "systemGray"
204
+ const match = baseClass.match(/^(text|bg|border)-(.+)$/);
205
+ if (!match) {
206
+ return [];
207
+ }
208
+
209
+ const [, prefix, colorName] = match;
210
+
211
+ // Build variant class names
212
+ const darkColorName = `${colorName}${darkSuffix}`;
213
+ const lightColorName = `${colorName}${lightSuffix}`;
214
+
215
+ // Validate that both color variants exist
216
+ const darkColorExists = customColors[darkColorName] !== undefined;
217
+ const lightColorExists = customColors[lightColorName] !== undefined;
218
+
219
+ if (!darkColorExists || !lightColorExists) {
220
+ if (process.env.NODE_ENV !== "production") {
221
+ const missing = [];
222
+ if (!darkColorExists) missing.push(`${colorName}${darkSuffix}`);
223
+ if (!lightColorExists) missing.push(`${colorName}${lightSuffix}`);
224
+ console.warn(
225
+ `[react-native-tailwind] scheme:${baseClass} requires both color variants to exist. ` +
226
+ `Missing: ${missing.join(", ")}. This modifier will be ignored.`,
227
+ );
228
+ }
229
+ return [];
230
+ }
231
+
232
+ // Expand to dark: and light: modifiers
233
+ return [
234
+ {
235
+ modifier: "dark" as ColorSchemeModifierType,
236
+ baseClass: `${prefix}-${darkColorName}`,
237
+ },
238
+ {
239
+ modifier: "light" as ColorSchemeModifierType,
240
+ baseClass: `${prefix}-${lightColorName}`,
241
+ },
242
+ ];
243
+ }
244
+
110
245
  /**
111
246
  * Split a space-separated className string into base and modifier classes
112
247
  *
@@ -164,8 +164,20 @@ function parseArbitraryLineHeight(value: string): number | null {
164
164
 
165
165
  /**
166
166
  * Parse typography classes
167
+ * @param cls - Class name to parse
168
+ * @param customFontFamily - Optional custom fontFamily from tailwind.config
167
169
  */
168
- export function parseTypography(cls: string): StyleObject | null {
170
+ export function parseTypography(cls: string, customFontFamily?: Record<string, string>): StyleObject | null {
171
+ // Merge custom fontFamily with defaults (custom takes precedence)
172
+ const fontFamilyMap = customFontFamily
173
+ ? {
174
+ ...FONT_FAMILY_MAP,
175
+ ...Object.fromEntries(
176
+ Object.entries(customFontFamily).map(([key, value]) => [`font-${key}`, { fontFamily: value }]),
177
+ ),
178
+ }
179
+ : FONT_FAMILY_MAP;
180
+
169
181
  // Font size: text-base, text-lg, text-[18px], etc.
170
182
  if (cls.startsWith("text-")) {
171
183
  const sizeKey = cls.substring(5);
@@ -202,7 +214,7 @@ export function parseTypography(cls: string): StyleObject | null {
202
214
 
203
215
  // Try each lookup table in order
204
216
  return (
205
- FONT_FAMILY_MAP[cls] ??
217
+ fontFamilyMap[cls] ??
206
218
  FONT_WEIGHT_MAP[cls] ??
207
219
  FONT_STYLE_MAP[cls] ??
208
220
  TEXT_ALIGN_MAP[cls] ??