@mgcrea/react-native-tailwind 0.4.0 → 0.5.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.
- package/README.md +443 -13
- package/dist/babel/index.cjs +804 -274
- package/dist/babel/index.d.ts +2 -1
- package/dist/babel/index.ts +319 -16
- package/dist/components/Pressable.d.ts +32 -0
- package/dist/components/Pressable.js +1 -0
- package/dist/components/TextInput.d.ts +56 -0
- package/dist/components/TextInput.js +1 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +1 -1
- package/dist/parser/aspectRatio.d.ts +16 -0
- package/dist/parser/aspectRatio.js +1 -0
- package/dist/parser/aspectRatio.test.d.ts +1 -0
- package/dist/parser/aspectRatio.test.js +1 -0
- package/dist/parser/borders.js +1 -1
- package/dist/parser/borders.test.d.ts +1 -0
- package/dist/parser/borders.test.js +1 -0
- package/dist/parser/colors.d.ts +1 -0
- package/dist/parser/colors.js +1 -1
- package/dist/parser/colors.test.d.ts +1 -0
- package/dist/parser/colors.test.js +1 -0
- package/dist/parser/index.d.ts +4 -0
- package/dist/parser/index.js +1 -1
- package/dist/parser/layout.d.ts +2 -0
- package/dist/parser/layout.js +1 -1
- package/dist/parser/layout.test.d.ts +1 -0
- package/dist/parser/layout.test.js +1 -0
- package/dist/parser/modifiers.d.ts +47 -0
- package/dist/parser/modifiers.js +1 -0
- package/dist/parser/modifiers.test.d.ts +1 -0
- package/dist/parser/modifiers.test.js +1 -0
- package/dist/parser/shadows.d.ts +26 -0
- package/dist/parser/shadows.js +1 -0
- package/dist/parser/shadows.test.d.ts +1 -0
- package/dist/parser/shadows.test.js +1 -0
- package/dist/parser/sizing.test.d.ts +1 -0
- package/dist/parser/sizing.test.js +1 -0
- package/dist/parser/spacing.d.ts +1 -1
- package/dist/parser/spacing.js +1 -1
- package/dist/parser/spacing.test.d.ts +1 -0
- package/dist/parser/spacing.test.js +1 -0
- package/dist/parser/typography.d.ts +2 -1
- package/dist/parser/typography.js +1 -1
- package/dist/parser/typography.test.d.ts +1 -0
- package/dist/parser/typography.test.js +1 -0
- package/dist/types.d.ts +5 -2
- package/package.json +7 -6
- package/src/babel/index.ts +319 -16
- package/src/components/Pressable.tsx +46 -0
- package/src/components/TextInput.tsx +90 -0
- package/src/index.ts +20 -2
- package/src/parser/aspectRatio.test.ts +191 -0
- package/src/parser/aspectRatio.ts +73 -0
- package/src/parser/borders.test.ts +329 -0
- package/src/parser/borders.ts +187 -108
- package/src/parser/colors.test.ts +335 -0
- package/src/parser/colors.ts +117 -6
- package/src/parser/index.ts +13 -2
- package/src/parser/layout.test.ts +459 -0
- package/src/parser/layout.ts +128 -0
- package/src/parser/modifiers.test.ts +375 -0
- package/src/parser/modifiers.ts +104 -0
- package/src/parser/shadows.test.ts +201 -0
- package/src/parser/shadows.ts +133 -0
- package/src/parser/sizing.test.ts +256 -0
- package/src/parser/spacing.test.ts +226 -0
- package/src/parser/spacing.ts +93 -138
- package/src/parser/typography.test.ts +221 -0
- package/src/parser/typography.ts +143 -112
- package/src/types.ts +2 -2
- package/dist/react-native.d.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mgcrea/react-native-tailwind",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Compile-time Tailwind CSS for React Native with zero runtime overhead",
|
|
5
5
|
"author": "Olivier Louvignes <olivier@mgcrea.io> (https://github.com/mgcrea)",
|
|
6
6
|
"homepage": "https://github.com/mgcrea/react-native-tailwind#readme",
|
|
@@ -33,14 +33,14 @@
|
|
|
33
33
|
"dev": "cd example; npm run dev",
|
|
34
34
|
"build": "npm run build:babel && npm run build:babel-plugin && npm run build:types",
|
|
35
35
|
"build:babel": "babel src --out-dir dist --extensions \".ts,.tsx,.js,.jsx\" --copy-files --ignore 'src/babel/**' --ignore '**/*.d.ts'",
|
|
36
|
-
"build:babel-plugin": "node scripts/bundle-babel-plugin.
|
|
37
|
-
"build:types": "tsc --emitDeclarationOnly && node scripts/post-build-types.
|
|
36
|
+
"build:babel-plugin": "node --experimental-strip-types scripts/bundle-babel-plugin.ts",
|
|
37
|
+
"build:types": "tsc --project tsconfig.build.json --emitDeclarationOnly && node --experimental-strip-types scripts/post-build-types.ts",
|
|
38
38
|
"install:ios": "cd example; npm run install:ios",
|
|
39
39
|
"open:ios": "cd example; npm run open:ios",
|
|
40
|
-
"lint": "eslint
|
|
40
|
+
"lint": "eslint src/",
|
|
41
41
|
"prettify": "prettier --write src/",
|
|
42
42
|
"check": "tsc --noEmit",
|
|
43
|
-
"spec": "
|
|
43
|
+
"spec": "vitest",
|
|
44
44
|
"test": "npm run lint && npm run check && npm run spec",
|
|
45
45
|
"prepare": "npm run build"
|
|
46
46
|
},
|
|
@@ -71,7 +71,8 @@
|
|
|
71
71
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
72
72
|
"react": "^19.2.0",
|
|
73
73
|
"react-native": "0.82.1",
|
|
74
|
-
"typescript": "^5.9.3"
|
|
74
|
+
"typescript": "^5.9.3",
|
|
75
|
+
"vitest": "^4.0.10"
|
|
75
76
|
},
|
|
76
77
|
"engines": {
|
|
77
78
|
"node": ">=18"
|
package/src/babel/index.ts
CHANGED
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
|
|
12
12
|
import type { NodePath, PluginObj, PluginPass } from "@babel/core";
|
|
13
13
|
import * as BabelTypes from "@babel/types";
|
|
14
|
-
import {
|
|
14
|
+
import { StyleObject } from "src/types.js";
|
|
15
|
+
import type { ModifierType, ParsedModifier } from "../parser/index.js";
|
|
16
|
+
import { parseClassName as parseClassNameFn, splitModifierClasses } from "../parser/index.js";
|
|
15
17
|
import { generateStyleKey as generateStyleKeyFn } from "../utils/styleKey.js";
|
|
16
18
|
import { extractCustomColors } from "./config-loader.js";
|
|
17
19
|
|
|
18
20
|
type PluginState = PluginPass & {
|
|
19
|
-
styleRegistry: Map<string,
|
|
21
|
+
styleRegistry: Map<string, StyleObject>;
|
|
20
22
|
hasClassNames: boolean;
|
|
21
23
|
hasStyleSheetImport: boolean;
|
|
22
24
|
customColors: Record<string, string>;
|
|
@@ -30,6 +32,7 @@ const STYLES_IDENTIFIER = "_twStyles";
|
|
|
30
32
|
*/
|
|
31
33
|
const SUPPORTED_CLASS_ATTRIBUTES = [
|
|
32
34
|
"className",
|
|
35
|
+
"containerClassName",
|
|
33
36
|
"contentContainerClassName",
|
|
34
37
|
"columnWrapperClassName",
|
|
35
38
|
"ListHeaderComponentClassName",
|
|
@@ -40,6 +43,9 @@ const SUPPORTED_CLASS_ATTRIBUTES = [
|
|
|
40
43
|
* Get the target style prop name based on the className attribute
|
|
41
44
|
*/
|
|
42
45
|
function getTargetStyleProp(attributeName: string): string {
|
|
46
|
+
if (attributeName === "containerClassName") {
|
|
47
|
+
return "containerStyle";
|
|
48
|
+
}
|
|
43
49
|
if (attributeName === "contentContainerClassName") {
|
|
44
50
|
return "contentContainerStyle";
|
|
45
51
|
}
|
|
@@ -55,6 +61,49 @@ function getTargetStyleProp(attributeName: string): string {
|
|
|
55
61
|
return "style";
|
|
56
62
|
}
|
|
57
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Check if a JSX element supports modifiers and determine which modifiers are supported
|
|
66
|
+
* Returns an object with component info and supported modifiers
|
|
67
|
+
*/
|
|
68
|
+
function getComponentModifierSupport(
|
|
69
|
+
jsxElement: any,
|
|
70
|
+
t: typeof BabelTypes,
|
|
71
|
+
): { component: string; supportedModifiers: ModifierType[] } | null {
|
|
72
|
+
if (!t.isJSXOpeningElement(jsxElement)) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const name = jsxElement.name;
|
|
77
|
+
let componentName: string | null = null;
|
|
78
|
+
|
|
79
|
+
// Handle simple identifier: <Pressable>
|
|
80
|
+
if (t.isJSXIdentifier(name)) {
|
|
81
|
+
componentName = name.name;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Handle member expression: <ReactNative.Pressable>
|
|
85
|
+
if (t.isJSXMemberExpression(name)) {
|
|
86
|
+
const property = name.property;
|
|
87
|
+
if (t.isJSXIdentifier(property)) {
|
|
88
|
+
componentName = property.name;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!componentName) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Map components to their supported modifiers
|
|
97
|
+
switch (componentName) {
|
|
98
|
+
case "Pressable":
|
|
99
|
+
return { component: "Pressable", supportedModifiers: ["active", "hover", "focus", "disabled"] };
|
|
100
|
+
case "TextInput":
|
|
101
|
+
return { component: "TextInput", supportedModifiers: ["focus", "disabled"] };
|
|
102
|
+
default:
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
58
107
|
/**
|
|
59
108
|
* Result of processing a dynamic expression
|
|
60
109
|
*/
|
|
@@ -242,6 +291,118 @@ function processStringOrExpression(node: any, state: PluginState, t: typeof Babe
|
|
|
242
291
|
return null;
|
|
243
292
|
}
|
|
244
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Process a static className string that contains modifiers
|
|
296
|
+
* Returns a style function expression for Pressable components
|
|
297
|
+
*/
|
|
298
|
+
function processStaticClassNameWithModifiers(
|
|
299
|
+
className: string,
|
|
300
|
+
state: PluginState,
|
|
301
|
+
t: typeof BabelTypes,
|
|
302
|
+
): any {
|
|
303
|
+
const { baseClasses, modifierClasses } = splitModifierClasses(className);
|
|
304
|
+
|
|
305
|
+
// Parse and register base classes
|
|
306
|
+
let baseStyleExpression: any = null;
|
|
307
|
+
if (baseClasses.length > 0) {
|
|
308
|
+
const baseClassName = baseClasses.join(" ");
|
|
309
|
+
const baseStyleObject = parseClassName(baseClassName, state.customColors);
|
|
310
|
+
const baseStyleKey = generateStyleKey(baseClassName);
|
|
311
|
+
state.styleRegistry.set(baseStyleKey, baseStyleObject);
|
|
312
|
+
baseStyleExpression = t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(baseStyleKey));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Parse and register modifier classes
|
|
316
|
+
// Group by modifier type for better organization
|
|
317
|
+
const modifiersByType = new Map<ModifierType, ParsedModifier[]>();
|
|
318
|
+
for (const mod of modifierClasses) {
|
|
319
|
+
if (!modifiersByType.has(mod.modifier)) {
|
|
320
|
+
modifiersByType.set(mod.modifier, []);
|
|
321
|
+
}
|
|
322
|
+
const modGroup = modifiersByType.get(mod.modifier);
|
|
323
|
+
if (modGroup) {
|
|
324
|
+
modGroup.push(mod);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Build style function: ({ pressed }) => [baseStyle, pressed && modifierStyle]
|
|
329
|
+
const styleArrayElements: any[] = [];
|
|
330
|
+
|
|
331
|
+
// Add base style first
|
|
332
|
+
if (baseStyleExpression) {
|
|
333
|
+
styleArrayElements.push(baseStyleExpression);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Add conditional styles for each modifier type
|
|
337
|
+
for (const [modifierType, modifiers] of modifiersByType) {
|
|
338
|
+
// Parse all modifier classes together
|
|
339
|
+
const modifierClassNames = modifiers.map((m) => m.baseClass).join(" ");
|
|
340
|
+
const modifierStyleObject = parseClassName(modifierClassNames, state.customColors);
|
|
341
|
+
const modifierStyleKey = generateStyleKey(`${modifierType}_${modifierClassNames}`);
|
|
342
|
+
state.styleRegistry.set(modifierStyleKey, modifierStyleObject);
|
|
343
|
+
|
|
344
|
+
// Create conditional: pressed && styles._active_bg_blue_700
|
|
345
|
+
const stateProperty = getStatePropertyForModifier(modifierType);
|
|
346
|
+
const conditionalExpression = t.logicalExpression(
|
|
347
|
+
"&&",
|
|
348
|
+
t.identifier(stateProperty),
|
|
349
|
+
t.memberExpression(t.identifier(STYLES_IDENTIFIER), t.identifier(modifierStyleKey)),
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
styleArrayElements.push(conditionalExpression);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// If only base style, return it directly; otherwise return array
|
|
356
|
+
if (styleArrayElements.length === 1) {
|
|
357
|
+
return styleArrayElements[0];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return t.arrayExpression(styleArrayElements);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Get the state property name for a modifier type
|
|
365
|
+
* Maps modifier types to component state parameter properties
|
|
366
|
+
*/
|
|
367
|
+
function getStatePropertyForModifier(modifier: ModifierType): string {
|
|
368
|
+
switch (modifier) {
|
|
369
|
+
case "active":
|
|
370
|
+
return "pressed";
|
|
371
|
+
case "hover":
|
|
372
|
+
return "hovered";
|
|
373
|
+
case "focus":
|
|
374
|
+
return "focused";
|
|
375
|
+
case "disabled":
|
|
376
|
+
return "disabled";
|
|
377
|
+
default:
|
|
378
|
+
return "pressed"; // fallback
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Create a style function for Pressable: ({ pressed }) => styleExpression
|
|
384
|
+
*/
|
|
385
|
+
function createStyleFunction(styleExpression: any, modifierTypes: ModifierType[], t: typeof BabelTypes): any {
|
|
386
|
+
// Build parameter object: { pressed, hovered, focused }
|
|
387
|
+
const paramProperties: any[] = [];
|
|
388
|
+
const usedStateProps = new Set<string>();
|
|
389
|
+
|
|
390
|
+
for (const modifierType of modifierTypes) {
|
|
391
|
+
const stateProperty = getStatePropertyForModifier(modifierType);
|
|
392
|
+
if (!usedStateProps.has(stateProperty)) {
|
|
393
|
+
usedStateProps.add(stateProperty);
|
|
394
|
+
paramProperties.push(
|
|
395
|
+
t.objectProperty(t.identifier(stateProperty), t.identifier(stateProperty), false, true),
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const param = t.objectPattern(paramProperties);
|
|
401
|
+
|
|
402
|
+
// Create arrow function: ({ pressed }) => styleExpression
|
|
403
|
+
return t.arrowFunctionExpression([param], styleExpression);
|
|
404
|
+
}
|
|
405
|
+
|
|
245
406
|
export default function reactNativeTailwindBabelPlugin({
|
|
246
407
|
types: t,
|
|
247
408
|
}: {
|
|
@@ -314,7 +475,7 @@ export default function reactNativeTailwindBabelPlugin({
|
|
|
314
475
|
// Determine target style prop based on attribute name
|
|
315
476
|
const targetStyleProp = getTargetStyleProp(attributeName);
|
|
316
477
|
|
|
317
|
-
// Handle static string literals
|
|
478
|
+
// Handle static string literals
|
|
318
479
|
if (t.isStringLiteral(value)) {
|
|
319
480
|
const className = value.value.trim();
|
|
320
481
|
|
|
@@ -326,13 +487,99 @@ export default function reactNativeTailwindBabelPlugin({
|
|
|
326
487
|
|
|
327
488
|
state.hasClassNames = true;
|
|
328
489
|
|
|
329
|
-
//
|
|
330
|
-
const
|
|
490
|
+
// Check if className contains modifiers (active:, hover:, focus:)
|
|
491
|
+
const { baseClasses, modifierClasses } = splitModifierClasses(className);
|
|
331
492
|
|
|
332
|
-
//
|
|
333
|
-
|
|
493
|
+
// If there are modifiers, check if this component supports them
|
|
494
|
+
if (modifierClasses.length > 0) {
|
|
495
|
+
// Get the JSX opening element (the direct parent of the attribute)
|
|
496
|
+
const jsxOpeningElement = path.parent;
|
|
497
|
+
const componentSupport = getComponentModifierSupport(jsxOpeningElement, t);
|
|
498
|
+
|
|
499
|
+
if (componentSupport) {
|
|
500
|
+
// Get modifier types used in className
|
|
501
|
+
const usedModifiers = Array.from(new Set(modifierClasses.map((m) => m.modifier)));
|
|
502
|
+
|
|
503
|
+
// Check if all modifiers are supported by this component
|
|
504
|
+
const unsupportedModifiers = usedModifiers.filter(
|
|
505
|
+
(mod) => !componentSupport.supportedModifiers.includes(mod),
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
if (unsupportedModifiers.length > 0) {
|
|
509
|
+
// Warn about unsupported modifiers
|
|
510
|
+
if (process.env.NODE_ENV !== "production") {
|
|
511
|
+
console.warn(
|
|
512
|
+
`[react-native-tailwind] Modifiers (${unsupportedModifiers.map((m) => `${m}:`).join(", ")}) are not supported on ${componentSupport.component} component at ${state.file.opts.filename ?? "unknown"}. ` +
|
|
513
|
+
`Supported modifiers: ${componentSupport.supportedModifiers.join(", ")}`,
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
// Filter out unsupported modifiers
|
|
517
|
+
const supportedModifierClasses = modifierClasses.filter((m) =>
|
|
518
|
+
componentSupport.supportedModifiers.includes(m.modifier),
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
// If no supported modifiers remain, fall through to normal processing
|
|
522
|
+
if (supportedModifierClasses.length === 0) {
|
|
523
|
+
// Continue to normal processing
|
|
524
|
+
} else {
|
|
525
|
+
// Process only supported modifiers
|
|
526
|
+
const filteredClassName =
|
|
527
|
+
baseClasses.join(" ") +
|
|
528
|
+
" " +
|
|
529
|
+
supportedModifierClasses.map((m) => `${m.modifier}:${m.baseClass}`).join(" ");
|
|
530
|
+
const styleExpression = processStaticClassNameWithModifiers(
|
|
531
|
+
filteredClassName.trim(),
|
|
532
|
+
state,
|
|
533
|
+
t,
|
|
534
|
+
);
|
|
535
|
+
const modifierTypes = Array.from(new Set(supportedModifierClasses.map((m) => m.modifier)));
|
|
536
|
+
const styleFunctionExpression = createStyleFunction(styleExpression, modifierTypes, t);
|
|
537
|
+
|
|
538
|
+
const parent = path.parent as any;
|
|
539
|
+
const styleAttribute = parent.attributes.find(
|
|
540
|
+
(attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
if (styleAttribute) {
|
|
544
|
+
mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
|
|
545
|
+
} else {
|
|
546
|
+
replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
|
|
547
|
+
}
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
} else {
|
|
551
|
+
// All modifiers are supported - process normally
|
|
552
|
+
const styleExpression = processStaticClassNameWithModifiers(className, state, t);
|
|
553
|
+
const modifierTypes = usedModifiers;
|
|
554
|
+
const styleFunctionExpression = createStyleFunction(styleExpression, modifierTypes, t);
|
|
555
|
+
|
|
556
|
+
const parent = path.parent as any;
|
|
557
|
+
const styleAttribute = parent.attributes.find(
|
|
558
|
+
(attr: any) => t.isJSXAttribute(attr) && attr.name.name === targetStyleProp,
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
if (styleAttribute) {
|
|
562
|
+
mergeStyleFunctionAttribute(path, styleAttribute, styleFunctionExpression, t);
|
|
563
|
+
} else {
|
|
564
|
+
replaceWithStyleFunctionAttribute(path, styleFunctionExpression, targetStyleProp, t);
|
|
565
|
+
}
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
// Component doesn't support any modifiers
|
|
570
|
+
if (process.env.NODE_ENV !== "production") {
|
|
571
|
+
const usedModifiers = Array.from(new Set(modifierClasses.map((m) => m.modifier)));
|
|
572
|
+
console.warn(
|
|
573
|
+
`[react-native-tailwind] Modifiers (${usedModifiers.map((m) => `${m}:`).join(", ")}) can only be used on compatible components (Pressable, TextInput). Found on unsupported element at ${state.file.opts.filename ?? "unknown"}`,
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
// Fall through to normal processing (ignore modifiers)
|
|
577
|
+
}
|
|
578
|
+
}
|
|
334
579
|
|
|
335
|
-
//
|
|
580
|
+
// Normal processing without modifiers
|
|
581
|
+
const styleObject = parseClassName(className, state.customColors);
|
|
582
|
+
const styleKey = generateStyleKey(className);
|
|
336
583
|
state.styleRegistry.set(styleKey, styleObject);
|
|
337
584
|
|
|
338
585
|
// Check if there's already a style prop on this element
|
|
@@ -505,13 +752,72 @@ function mergeDynamicStyleAttribute(
|
|
|
505
752
|
}
|
|
506
753
|
|
|
507
754
|
/**
|
|
508
|
-
*
|
|
755
|
+
* Replace className with style function attribute (for Pressable with modifiers)
|
|
756
|
+
*/
|
|
757
|
+
function replaceWithStyleFunctionAttribute(
|
|
758
|
+
classNamePath: NodePath,
|
|
759
|
+
styleFunctionExpression: any,
|
|
760
|
+
targetStyleProp: string,
|
|
761
|
+
t: typeof BabelTypes,
|
|
762
|
+
) {
|
|
763
|
+
const styleAttribute = t.jsxAttribute(
|
|
764
|
+
t.jsxIdentifier(targetStyleProp),
|
|
765
|
+
t.jsxExpressionContainer(styleFunctionExpression),
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
classNamePath.replaceWith(styleAttribute);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Merge className style function with existing style prop (for Pressable with modifiers)
|
|
509
773
|
*/
|
|
510
|
-
function
|
|
511
|
-
|
|
512
|
-
|
|
774
|
+
function mergeStyleFunctionAttribute(
|
|
775
|
+
classNamePath: NodePath,
|
|
776
|
+
styleAttribute: any,
|
|
777
|
+
styleFunctionExpression: any,
|
|
513
778
|
t: typeof BabelTypes,
|
|
514
779
|
) {
|
|
780
|
+
const existingStyle = styleAttribute.value.expression;
|
|
781
|
+
|
|
782
|
+
// Create a wrapper function that merges both styles
|
|
783
|
+
// ({ pressed }) => [styleFunctionResult, existingStyle]
|
|
784
|
+
// We need to call the style function and merge results
|
|
785
|
+
|
|
786
|
+
// If existing is already a function, we need to handle it specially
|
|
787
|
+
if (t.isArrowFunctionExpression(existingStyle) || t.isFunctionExpression(existingStyle)) {
|
|
788
|
+
// Both are functions - create wrapper that calls both
|
|
789
|
+
// (_state) => [newStyleFn(_state), existingStyleFn(_state)]
|
|
790
|
+
// Create an identifier for the parameter to pass to the function calls
|
|
791
|
+
const paramIdentifier = t.identifier("_state");
|
|
792
|
+
|
|
793
|
+
const newFunctionCall = t.callExpression(styleFunctionExpression, [paramIdentifier]);
|
|
794
|
+
const existingFunctionCall = t.callExpression(existingStyle, [paramIdentifier]);
|
|
795
|
+
|
|
796
|
+
const mergedArray = t.arrayExpression([newFunctionCall, existingFunctionCall]);
|
|
797
|
+
const wrapperFunction = t.arrowFunctionExpression([paramIdentifier], mergedArray);
|
|
798
|
+
|
|
799
|
+
styleAttribute.value = t.jsxExpressionContainer(wrapperFunction);
|
|
800
|
+
} else {
|
|
801
|
+
// Existing is static - create function that returns array
|
|
802
|
+
// (_state) => [styleFunctionResult, existingStyle]
|
|
803
|
+
// Create an identifier for the parameter to pass to the function call
|
|
804
|
+
const paramIdentifier = t.identifier("_state");
|
|
805
|
+
|
|
806
|
+
const functionCall = t.callExpression(styleFunctionExpression, [paramIdentifier]);
|
|
807
|
+
const mergedArray = t.arrayExpression([functionCall, existingStyle]);
|
|
808
|
+
const wrapperFunction = t.arrowFunctionExpression([paramIdentifier], mergedArray);
|
|
809
|
+
|
|
810
|
+
styleAttribute.value = t.jsxExpressionContainer(wrapperFunction);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Remove the className attribute
|
|
814
|
+
classNamePath.remove();
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Inject StyleSheet.create with all collected styles
|
|
819
|
+
*/
|
|
820
|
+
function injectStyles(path: NodePath, styleRegistry: Map<string, StyleObject>, t: typeof BabelTypes) {
|
|
515
821
|
// Build style object properties
|
|
516
822
|
const styleProperties: any[] = [];
|
|
517
823
|
|
|
@@ -549,10 +855,7 @@ function injectStyles(
|
|
|
549
855
|
}
|
|
550
856
|
|
|
551
857
|
// Helper functions that use the imported parser
|
|
552
|
-
function parseClassName(
|
|
553
|
-
className: string,
|
|
554
|
-
customColors: Record<string, string>,
|
|
555
|
-
): Record<string, string | number> {
|
|
858
|
+
function parseClassName(className: string, customColors: Record<string, string>): StyleObject {
|
|
556
859
|
return parseClassNameFn(className, customColors);
|
|
557
860
|
}
|
|
558
861
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Pressable component with modifier support
|
|
3
|
+
* Injects disabled state into style function for disabled: modifier support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ComponentRef } from "react";
|
|
7
|
+
import { forwardRef } from "react";
|
|
8
|
+
import {
|
|
9
|
+
Pressable as RNPressable,
|
|
10
|
+
type PressableStateCallbackType,
|
|
11
|
+
type PressableProps as RNPressableProps,
|
|
12
|
+
type StyleProp,
|
|
13
|
+
type ViewStyle,
|
|
14
|
+
} from "react-native";
|
|
15
|
+
|
|
16
|
+
// Extend PressableStateCallbackType to include disabled
|
|
17
|
+
type EnhancedPressableState = PressableStateCallbackType & { disabled: boolean | null | undefined };
|
|
18
|
+
|
|
19
|
+
export type PressableProps = Omit<RNPressableProps, "style"> & {
|
|
20
|
+
/**
|
|
21
|
+
* Style can be a static style object/array or a function that receives Pressable state + disabled
|
|
22
|
+
*/
|
|
23
|
+
style?: StyleProp<ViewStyle> | ((state: EnhancedPressableState) => StyleProp<ViewStyle>);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Enhanced Pressable that supports the disabled: modifier
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* <Pressable
|
|
31
|
+
* disabled={isLoading}
|
|
32
|
+
* className="bg-blue-500 active:bg-blue-700 disabled:bg-gray-400"
|
|
33
|
+
* >
|
|
34
|
+
* <Text>Submit</Text>
|
|
35
|
+
* </Pressable>
|
|
36
|
+
*/
|
|
37
|
+
export const Pressable = forwardRef<ComponentRef<typeof RNPressable>, PressableProps>(function Pressable(
|
|
38
|
+
{ style, disabled = false, ...props },
|
|
39
|
+
ref,
|
|
40
|
+
) {
|
|
41
|
+
// Inject disabled into style function context
|
|
42
|
+
const resolvedStyle =
|
|
43
|
+
typeof style === "function" ? (state: PressableStateCallbackType) => style({ ...state, disabled }) : style;
|
|
44
|
+
|
|
45
|
+
return <RNPressable ref={ref} disabled={disabled} style={resolvedStyle} {...props} />;
|
|
46
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced TextInput component with focus state support for focus: modifier
|
|
3
|
+
*
|
|
4
|
+
* This component wraps React Native's TextInput and manages focus state internally,
|
|
5
|
+
* allowing the style prop to be a function that receives { focused: boolean }.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { TextInput } from '@mgcrea/react-native-tailwind';
|
|
10
|
+
*
|
|
11
|
+
* <TextInput
|
|
12
|
+
* className="border-2 border-gray-300 focus:border-blue-500 p-3 rounded-lg"
|
|
13
|
+
* placeholder="Email"
|
|
14
|
+
* />
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { forwardRef, useCallback, useState } from "react";
|
|
19
|
+
import {
|
|
20
|
+
type BlurEvent,
|
|
21
|
+
type FocusEvent,
|
|
22
|
+
TextInput as RNTextInput,
|
|
23
|
+
type TextInputProps as RNTextInputProps,
|
|
24
|
+
} from "react-native";
|
|
25
|
+
|
|
26
|
+
export type TextInputProps = Omit<RNTextInputProps, "style"> & {
|
|
27
|
+
/**
|
|
28
|
+
* Style can be a static style object/array or a function that receives focus and disabled state
|
|
29
|
+
*/
|
|
30
|
+
style?:
|
|
31
|
+
| RNTextInputProps["style"]
|
|
32
|
+
| ((state: { focused: boolean; disabled: boolean }) => RNTextInputProps["style"]);
|
|
33
|
+
/**
|
|
34
|
+
* Convenience prop for disabled state (overrides editable if provided)
|
|
35
|
+
* When true, sets editable to false
|
|
36
|
+
*/
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Enhanced TextInput with focus and disabled state support
|
|
42
|
+
*
|
|
43
|
+
* Manages focus state internally and passes it to style functions,
|
|
44
|
+
* enabling the use of focus: and disabled: modifiers in className.
|
|
45
|
+
*
|
|
46
|
+
* Note: TextInput uses `editable` prop internally. You can pass either:
|
|
47
|
+
* - `disabled={true}` - convenience prop (sets editable to false)
|
|
48
|
+
* - `editable={false}` - React Native's native prop
|
|
49
|
+
* If both are provided, `disabled` takes precedence.
|
|
50
|
+
*/
|
|
51
|
+
export const TextInput = forwardRef<RNTextInput, TextInputProps>(function TextInput(
|
|
52
|
+
{ style, onFocus, onBlur, disabled, editable = true, ...props },
|
|
53
|
+
ref,
|
|
54
|
+
) {
|
|
55
|
+
const [focused, setFocused] = useState(false);
|
|
56
|
+
|
|
57
|
+
const handleFocus = useCallback(
|
|
58
|
+
(e: FocusEvent) => {
|
|
59
|
+
setFocused(true);
|
|
60
|
+
onFocus?.(e);
|
|
61
|
+
},
|
|
62
|
+
[onFocus],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const handleBlur = useCallback(
|
|
66
|
+
(e: BlurEvent) => {
|
|
67
|
+
setFocused(false);
|
|
68
|
+
onBlur?.(e);
|
|
69
|
+
},
|
|
70
|
+
[onBlur],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Resolve editable state: disabled prop overrides editable if provided
|
|
74
|
+
const isEditable = disabled !== undefined ? !disabled : editable;
|
|
75
|
+
const isDisabled = !isEditable;
|
|
76
|
+
|
|
77
|
+
// Resolve style - call function with focus and disabled state if needed
|
|
78
|
+
const resolvedStyle = typeof style === "function" ? style({ focused, disabled: isDisabled }) : style;
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<RNTextInput
|
|
82
|
+
ref={ref}
|
|
83
|
+
style={resolvedStyle}
|
|
84
|
+
editable={isEditable}
|
|
85
|
+
onFocus={handleFocus}
|
|
86
|
+
onBlur={handleBlur}
|
|
87
|
+
{...props}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -11,10 +11,28 @@ export { generateStyleKey } from "./utils/styleKey";
|
|
|
11
11
|
export type { RNStyle, StyleObject } from "./types";
|
|
12
12
|
|
|
13
13
|
// Re-export individual parsers for advanced usage
|
|
14
|
-
export {
|
|
14
|
+
export {
|
|
15
|
+
parseAspectRatio,
|
|
16
|
+
parseBorder,
|
|
17
|
+
parseColor,
|
|
18
|
+
parseLayout,
|
|
19
|
+
parseShadow,
|
|
20
|
+
parseSizing,
|
|
21
|
+
parseSpacing,
|
|
22
|
+
parseTypography,
|
|
23
|
+
} from "./parser";
|
|
15
24
|
|
|
16
25
|
// Re-export constants for customization
|
|
26
|
+
export { ASPECT_RATIO_PRESETS } from "./parser/aspectRatio";
|
|
17
27
|
export { COLORS } from "./parser/colors";
|
|
28
|
+
export { INSET_SCALE, Z_INDEX_SCALE } from "./parser/layout";
|
|
29
|
+
export { SHADOW_SCALE } from "./parser/shadows";
|
|
18
30
|
export { SIZE_PERCENTAGES, SIZE_SCALE } from "./parser/sizing";
|
|
19
31
|
export { SPACING_SCALE } from "./parser/spacing";
|
|
20
|
-
export { FONT_SIZES } from "./parser/typography";
|
|
32
|
+
export { FONT_SIZES, LETTER_SPACING_SCALE } from "./parser/typography";
|
|
33
|
+
|
|
34
|
+
// Re-export enhanced components with modifier support
|
|
35
|
+
export { Pressable } from "./components/Pressable";
|
|
36
|
+
export type { PressableProps } from "./components/Pressable";
|
|
37
|
+
export { TextInput } from "./components/TextInput";
|
|
38
|
+
export type { TextInputProps } from "./components/TextInput";
|