react-native-twc 1.1.3 → 1.2.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 +104 -0
- package/dist/index.d.ts +35 -2
- package/dist/index.js +42 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ A lightweight library for creating styled React Native components using Tailwind
|
|
|
12
12
|
- 🦄 **Works with any React Native component**
|
|
13
13
|
- 🚀 **First-class `tailwind-merge` and `cva` support**
|
|
14
14
|
- 📱 **Built for React Native + NativeWind**
|
|
15
|
+
- 🎁 **`withChildren`** — Pre-define children rendering with type safety
|
|
16
|
+
- 🔀 **Smart style merging** — `attrs` styles merge with props styles
|
|
15
17
|
|
|
16
18
|
## Installation
|
|
17
19
|
|
|
@@ -209,6 +211,73 @@ const Box = twc(View).transientProps(["size"])<Props>((props) => ({
|
|
|
209
211
|
<Box size="lg" />
|
|
210
212
|
```
|
|
211
213
|
|
|
214
|
+
### Using `withChildren`
|
|
215
|
+
|
|
216
|
+
Pre-define how children should be rendered. Useful for creating button components with consistent text styling:
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
import { Pressable, Text } from "react-native";
|
|
220
|
+
import { twx } from "react-native-twc";
|
|
221
|
+
|
|
222
|
+
// Default: accepts any ReactNode
|
|
223
|
+
const Card = twx(View).withChildren((children) => (
|
|
224
|
+
<View className="p-4">{children}</View>
|
|
225
|
+
))`bg-white rounded-lg`;
|
|
226
|
+
|
|
227
|
+
// With generic type: accepts only string
|
|
228
|
+
const Button = twx(Pressable).withChildren<string>((text) => (
|
|
229
|
+
<Text className="text-white font-bold text-center">{text}</Text>
|
|
230
|
+
))`bg-blue-500 py-3 px-6 rounded-lg`;
|
|
231
|
+
|
|
232
|
+
// Usage
|
|
233
|
+
<Button>Submit</Button> // text is typed as string
|
|
234
|
+
<Card><CustomComponent /></Card> // children is ReactNode
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Combine with `attrs` for powerful component composition:
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
const FloatButton = twx(Pressable)
|
|
241
|
+
.attrs({
|
|
242
|
+
activeOpacity: 0.8,
|
|
243
|
+
style: { shadowColor: "#000", shadowOffset: { width: 0, height: 2 } },
|
|
244
|
+
})
|
|
245
|
+
.withChildren<string>((text) => (
|
|
246
|
+
<Text className="text-white text-lg">{text}</Text>
|
|
247
|
+
))`absolute bottom-4 right-4 bg-purple-500 rounded-full p-4`;
|
|
248
|
+
|
|
249
|
+
<FloatButton>Add</FloatButton>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Smart Style Merging with `attrs`
|
|
253
|
+
|
|
254
|
+
When using `attrs` with a `style` prop, styles are intelligently merged (not replaced) when you pass additional styles:
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
const Card = twc(View).attrs({
|
|
258
|
+
style: { backgroundColor: "white", padding: 16, borderRadius: 8 },
|
|
259
|
+
})`shadow-lg`;
|
|
260
|
+
|
|
261
|
+
// Styles are merged: padding and borderRadius are preserved
|
|
262
|
+
<Card style={{ backgroundColor: "blue", margin: 10 }}>
|
|
263
|
+
Content
|
|
264
|
+
</Card>
|
|
265
|
+
// Result: { backgroundColor: "blue", padding: 16, borderRadius: 8, margin: 10 }
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
This also works with dynamic attrs:
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
type Props = TwcComponentProps<typeof View> & { $padded?: boolean };
|
|
272
|
+
|
|
273
|
+
const Box = twc(View).attrs<Props>((props) => ({
|
|
274
|
+
style: { padding: props.$padded ? 20 : 0 },
|
|
275
|
+
}))`bg-gray-100`;
|
|
276
|
+
|
|
277
|
+
<Box $padded style={{ margin: 10 }}>Content</Box>
|
|
278
|
+
// Result: { padding: 20, margin: 10 }
|
|
279
|
+
```
|
|
280
|
+
|
|
212
281
|
## API Reference
|
|
213
282
|
|
|
214
283
|
### `twc(Component)`
|
|
@@ -258,12 +327,47 @@ import { twx } from "react-native-twc";
|
|
|
258
327
|
const Title = twx(Text)`font-bold`;
|
|
259
328
|
```
|
|
260
329
|
|
|
330
|
+
### `.withChildren<T>(renderer)`
|
|
331
|
+
|
|
332
|
+
Pre-define children rendering with optional type constraint.
|
|
333
|
+
|
|
334
|
+
```tsx
|
|
335
|
+
// Accept any ReactNode (default)
|
|
336
|
+
const Card = twc(View).withChildren((children) => (
|
|
337
|
+
<Wrapper>{children}</Wrapper>
|
|
338
|
+
))`bg-white`;
|
|
339
|
+
|
|
340
|
+
// Accept only string
|
|
341
|
+
const Button = twc(Pressable).withChildren<string>((text) => (
|
|
342
|
+
<Text className="text-white">{text}</Text>
|
|
343
|
+
))`bg-blue-500`;
|
|
344
|
+
|
|
345
|
+
// Accept string or undefined
|
|
346
|
+
const Label = twc(View).withChildren<string | undefined>((text) => (
|
|
347
|
+
<Text>{text ?? "Default"}</Text>
|
|
348
|
+
))`p-2`;
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### `.attrs(attributes)` - Style Merging
|
|
352
|
+
|
|
353
|
+
When `attrs` includes a `style` prop, it will be merged with any `style` passed to the component (not replaced):
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
const Box = twc(View).attrs({
|
|
357
|
+
style: { padding: 10 }, // Base style
|
|
358
|
+
})`bg-white`;
|
|
359
|
+
|
|
360
|
+
<Box style={{ margin: 5 }} /> // Merged: { padding: 10, margin: 5 }
|
|
361
|
+
```
|
|
362
|
+
|
|
261
363
|
## Differences from TWC (Web)
|
|
262
364
|
|
|
263
365
|
| Feature | TWC (Web) | react-native-twc |
|
|
264
366
|
|---------|-----------|------------------|
|
|
265
367
|
| HTML tags (`twc.div`) | ✅ Supported | ❌ Not supported |
|
|
266
368
|
| `asChild` prop | ✅ Supported | ❌ Not supported |
|
|
369
|
+
| `withChildren` | ❌ Not supported | ✅ Supported |
|
|
370
|
+
| Smart style merging | ❌ Not supported | ✅ Supported |
|
|
267
371
|
| React Native components | ❌ Not optimized | ✅ Fully supported |
|
|
268
372
|
| NativeWind | ❌ Not designed for | ✅ First-class support |
|
|
269
373
|
|
package/dist/index.d.ts
CHANGED
|
@@ -25,9 +25,31 @@ type ResultProps<TComponent extends RNComponentType, TProps, TExtraProps, TCompo
|
|
|
25
25
|
* 支持模板字符串语法和函数形式
|
|
26
26
|
*/
|
|
27
27
|
type Template<TComponent extends RNComponentType, TCompose extends AbstractCompose, TExtraProps, TParentProps = undefined> = <TProps = TParentProps>(strings: TemplateStringsArray | ((props: ResultProps<TComponent, TProps, TExtraProps, TCompose>) => 'className' extends keyof TProps ? TProps['className'] : Parameters<TCompose>[0]), ...values: any[]) => React.ForwardRefExoticComponent<ResultProps<TComponent, TProps, TExtraProps, TCompose>>;
|
|
28
|
+
/**
|
|
29
|
+
* Children 渲染器类型
|
|
30
|
+
* 用于包裹 children 并返回 ReactNode
|
|
31
|
+
* TChildren 默认为 React.ReactNode,但可以指定为更具体的类型如 string
|
|
32
|
+
*/
|
|
33
|
+
type ChildrenRenderer<TChildren = React.ReactNode> = (children: TChildren) => React.ReactNode;
|
|
34
|
+
/**
|
|
35
|
+
* 带 withChildren 的模板类型
|
|
36
|
+
* 用于 attrs 返回后仍可调用 withChildren
|
|
37
|
+
*/
|
|
38
|
+
type TemplateWithChildren<TComponent extends RNComponentType, TCompose extends AbstractCompose, TExtraProps, TParentProps = undefined> = Template<TComponent, TCompose, TExtraProps, TParentProps> & {
|
|
39
|
+
/**
|
|
40
|
+
* 预定义 children 的渲染方式
|
|
41
|
+
* 可通过泛型指定 children 类型,默认为 React.ReactNode
|
|
42
|
+
* @example
|
|
43
|
+
* // 默认接受任意 ReactNode
|
|
44
|
+
* const Card = twc(View).withChildren((children) => <Wrapper>{children}</Wrapper>)`...`
|
|
45
|
+
* // 指定只接受 string
|
|
46
|
+
* const Button = twc(Pressable).withChildren<string>((text) => <Text>{text}</Text>)`...`
|
|
47
|
+
*/
|
|
48
|
+
withChildren: <TChildren = React.ReactNode>(renderer: ChildrenRenderer<TChildren>) => Template<TComponent, TCompose, TExtraProps, TParentProps>;
|
|
49
|
+
};
|
|
28
50
|
/**
|
|
29
51
|
* 第一级模板类型
|
|
30
|
-
* 包含 attrs 和
|
|
52
|
+
* 包含 attrs、transientProps 和 withChildren 方法
|
|
31
53
|
*/
|
|
32
54
|
type FirstLevelTemplate<TComponent extends RNComponentType, TCompose extends AbstractCompose, TExtraProps> = Template<TComponent, TCompose, TExtraProps> & {
|
|
33
55
|
/**
|
|
@@ -35,7 +57,7 @@ type FirstLevelTemplate<TComponent extends RNComponentType, TCompose extends Abs
|
|
|
35
57
|
* @example
|
|
36
58
|
* const TextInput = twc(RNTextInput).attrs({ keyboardType: 'email-address' })`...`
|
|
37
59
|
*/
|
|
38
|
-
attrs: <TProps = undefined>(attrs: (Omit<Partial<ComponentProps<TComponent>>, 'className'> & Record<string, any>) | ((props: ResultProps<TComponent, TProps, TExtraProps, TCompose>) => Record<string, any>)) =>
|
|
60
|
+
attrs: <TProps = undefined>(attrs: (Omit<Partial<ComponentProps<TComponent>>, 'className'> & Record<string, any>) | ((props: ResultProps<TComponent, TProps, TExtraProps, TCompose>) => Record<string, any>)) => TemplateWithChildren<TComponent, TCompose, TExtraProps, TProps>;
|
|
39
61
|
} & {
|
|
40
62
|
/**
|
|
41
63
|
* 防止特定 props 被转发到底层组件
|
|
@@ -43,6 +65,17 @@ type FirstLevelTemplate<TComponent extends RNComponentType, TCompose extends Abs
|
|
|
43
65
|
* const Title = twc(Text).transientProps(['$size'])`...`
|
|
44
66
|
*/
|
|
45
67
|
transientProps: (fn: string[] | ((prop: string) => boolean)) => FirstLevelTemplate<TComponent, TCompose, TExtraProps>;
|
|
68
|
+
} & {
|
|
69
|
+
/**
|
|
70
|
+
* 预定义 children 的渲染方式
|
|
71
|
+
* 可通过泛型指定 children 类型,默认为 React.ReactNode
|
|
72
|
+
* @example
|
|
73
|
+
* // 默认接受任意 ReactNode
|
|
74
|
+
* const Card = twc(View).withChildren((children) => <Wrapper>{children}</Wrapper>)`...`
|
|
75
|
+
* // 指定只接受 string
|
|
76
|
+
* const Button = twc(Pressable).withChildren<string>((text) => <Text>{text}</Text>)`...`
|
|
77
|
+
*/
|
|
78
|
+
withChildren: <TChildren = React.ReactNode>(renderer: ChildrenRenderer<TChildren>) => FirstLevelTemplate<TComponent, TCompose, TExtraProps>;
|
|
46
79
|
};
|
|
47
80
|
/**
|
|
48
81
|
* TWC 主类型
|
package/dist/index.js
CHANGED
|
@@ -13,11 +13,38 @@ function filterProps(props, shouldForwardProp) {
|
|
|
13
13
|
return filteredProps;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function flattenStyle(style) {
|
|
17
|
+
if (style == null) return {};
|
|
18
|
+
if (Array.isArray(style)) return style.reduce((acc, s) => ({
|
|
19
|
+
...acc,
|
|
20
|
+
...flattenStyle(s)
|
|
21
|
+
}), {});
|
|
22
|
+
return style;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function mergeStyles(baseStyle, overrideStyle) {
|
|
26
|
+
if (baseStyle == null) return overrideStyle;
|
|
27
|
+
if (overrideStyle == null) return baseStyle;
|
|
28
|
+
return {
|
|
29
|
+
...flattenStyle(baseStyle),
|
|
30
|
+
...flattenStyle(overrideStyle)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function mergePropsWithAttrs(attrs, props) {
|
|
35
|
+
const merged = {
|
|
36
|
+
...attrs,
|
|
37
|
+
...props
|
|
38
|
+
};
|
|
39
|
+
if (attrs.style != null && props.style != null) merged.style = mergeStyles(attrs.style, props.style);
|
|
40
|
+
return merged;
|
|
41
|
+
}
|
|
42
|
+
|
|
16
43
|
const createTwc = (config = {}) => {
|
|
17
44
|
const compose = config.compose || clsx;
|
|
18
45
|
const defaultShouldForwardProp = config.shouldForwardProp || ((prop) => prop[0] !== "$");
|
|
19
46
|
const wrap = (Component) => {
|
|
20
|
-
const createTemplate = (attrs, shouldForwardProp = defaultShouldForwardProp) => {
|
|
47
|
+
const createTemplate = (attrs, shouldForwardProp = defaultShouldForwardProp, childrenRenderer) => {
|
|
21
48
|
const componentCache = /* @__PURE__ */ new Map();
|
|
22
49
|
const template = (stringsOrFn, ...values) => {
|
|
23
50
|
const isClassFn = typeof stringsOrFn === "function";
|
|
@@ -28,22 +55,19 @@ const createTwc = (config = {}) => {
|
|
|
28
55
|
const isAttrsFunction = typeof attrs === "function";
|
|
29
56
|
const staticAttrs = !isAttrsFunction && hasAttrs ? attrs : void 0;
|
|
30
57
|
const ForwardedComponent = React.forwardRef((p, ref) => {
|
|
31
|
-
const { className: classNameProp, ...rest } = p;
|
|
58
|
+
const { className: classNameProp, children, ...rest } = p;
|
|
32
59
|
let finalProps;
|
|
33
60
|
if (!hasAttrs) finalProps = filterProps(rest, shouldForwardProp);
|
|
34
|
-
else if (isAttrsFunction) finalProps = filterProps(
|
|
35
|
-
|
|
36
|
-
...rest
|
|
37
|
-
}, shouldForwardProp);
|
|
38
|
-
else finalProps = filterProps({
|
|
39
|
-
...staticAttrs,
|
|
40
|
-
...rest
|
|
41
|
-
}, shouldForwardProp);
|
|
61
|
+
else if (isAttrsFunction) finalProps = filterProps(mergePropsWithAttrs(attrs(p), rest), shouldForwardProp);
|
|
62
|
+
else finalProps = filterProps(mergePropsWithAttrs(staticAttrs, rest), shouldForwardProp);
|
|
42
63
|
const baseClassName = isClassFn ? stringsOrFn(p) : tplClassName;
|
|
64
|
+
const finalClassName = typeof baseClassName === "function" ? (renderProps) => compose(baseClassName(renderProps), typeof classNameProp === "function" ? classNameProp(renderProps) : classNameProp) : compose(baseClassName, classNameProp);
|
|
65
|
+
const finalChildren = childrenRenderer ? childrenRenderer(children) : children;
|
|
43
66
|
return /* @__PURE__ */ jsx(Component, {
|
|
44
67
|
ref,
|
|
45
|
-
className:
|
|
46
|
-
...finalProps
|
|
68
|
+
className: finalClassName,
|
|
69
|
+
...finalProps,
|
|
70
|
+
children: finalChildren
|
|
47
71
|
});
|
|
48
72
|
});
|
|
49
73
|
ForwardedComponent.displayName = `twc(${Component.displayName || Component.name || "Component"})`;
|
|
@@ -51,10 +75,13 @@ const createTwc = (config = {}) => {
|
|
|
51
75
|
return ForwardedComponent;
|
|
52
76
|
};
|
|
53
77
|
template.transientProps = (fnOrArray) => {
|
|
54
|
-
return createTemplate(attrs, typeof fnOrArray === "function" ? (prop) => !fnOrArray(prop) : (prop) => !fnOrArray.includes(prop));
|
|
78
|
+
return createTemplate(attrs, typeof fnOrArray === "function" ? (prop) => !fnOrArray(prop) : (prop) => !fnOrArray.includes(prop), childrenRenderer);
|
|
79
|
+
};
|
|
80
|
+
template.withChildren = (renderer) => {
|
|
81
|
+
return createTemplate(attrs, shouldForwardProp, renderer);
|
|
55
82
|
};
|
|
56
|
-
if (attrs === void 0) template.attrs = (
|
|
57
|
-
return createTemplate(
|
|
83
|
+
if (attrs === void 0) template.attrs = (newAttrs) => {
|
|
84
|
+
return createTemplate(newAttrs, shouldForwardProp, childrenRenderer);
|
|
58
85
|
};
|
|
59
86
|
return template;
|
|
60
87
|
};
|