react-native-twc 1.1.2 → 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 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 和 transientProps 方法
52
+ * 包含 attrs、transientPropswithChildren 方法
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>)) => Template<TComponent, TCompose, TExtraProps, TProps>;
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,12 +13,39 @@ 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) => {
21
- const componentCache = new Map();
47
+ const createTemplate = (attrs, shouldForwardProp = defaultShouldForwardProp, childrenRenderer) => {
48
+ const componentCache = /* @__PURE__ */ new Map();
22
49
  const template = (stringsOrFn, ...values) => {
23
50
  const isClassFn = typeof stringsOrFn === "function";
24
51
  const cacheKey = isClassFn ? stringsOrFn.toString() : String.raw({ raw: stringsOrFn }, ...values);
@@ -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
- ...attrs(p),
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;
43
- return jsx(Component, {
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;
66
+ return /* @__PURE__ */ jsx(Component, {
44
67
  ref,
45
- className: typeof baseClassName === "function" ? (renderProps) => compose(baseClassName(renderProps), typeof classNameProp === "function" ? classNameProp(renderProps) : classNameProp) : compose(baseClassName, classNameProp),
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 = (attrs$1) => {
57
- return createTemplate(attrs$1, shouldForwardProp);
83
+ if (attrs === void 0) template.attrs = (newAttrs) => {
84
+ return createTemplate(newAttrs, shouldForwardProp, childrenRenderer);
58
85
  };
59
86
  return template;
60
87
  };
package/package.json CHANGED
@@ -1,76 +1,70 @@
1
1
  {
2
- "name": "react-native-twc",
3
- "version": "1.1.2",
4
- "description": "Create reusable React Native + NativeWind components with Tailwind CSS syntax.",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "types": "./dist/index.d.ts",
12
- "default": "./dist/index.js"
13
- }
14
- },
15
- "scripts": {
16
- "test": "bun test",
17
- "build": "rm -rf dist && rolldown -c && tsc -p tsconfig.build.json",
18
- "check-types": "tsc --noEmit",
19
- "prepublishOnly": "bun test && bun run build"
20
- },
21
- "keywords": [
22
- "react-native",
23
- "nativewind",
24
- "tailwind",
25
- "css",
26
- "components",
27
- "styled",
28
- "styled-components",
29
- "expo"
30
- ],
31
- "author": "ldystudio",
32
- "license": "MIT",
33
- "repository": {
34
- "type": "git",
35
- "url": "git+https://github.com/ldystudio/react-native-twc.git"
36
- },
37
- "homepage": "https://react-twc.vercel.app",
38
- "devDependencies": {
39
- "@eslint/eslintrc": "^3.3.1",
40
- "@eslint/js": "^9.30.0",
41
- "@happy-dom/global-registrator": "^18.0.1",
42
- "@testing-library/react": "^16.3.0",
43
- "@types/bun": "^1.2.5",
44
- "@types/react": "^19.1.8",
45
- "@typescript-eslint/eslint-plugin": "^8.35.0",
46
- "@typescript-eslint/parser": "^8.35.0",
47
- "class-variance-authority": "^0.7.1",
48
- "eslint": "^9.30.0",
49
- "happy-dom": "^18.0.1",
50
- "prettier": "^3.6.2",
51
- "react": "^19.1.0",
52
- "react-dom": "^19.1.0",
53
- "rolldown": "^1.0.0-beta.59",
54
- "tailwind-merge": "^3.3.1",
55
- "typescript": "^5.8.3",
56
- "typescript-eslint": "^8.35.0",
57
- "ultracite": "7.0.10"
58
- },
59
- "dependencies": {
60
- "clsx": "^2.1.1"
61
- },
62
- "peerDependencies": {
63
- "react": ">=18.0.0",
64
- "react-native": ">=0.72.0",
65
- "nativewind": ">=4.0.0",
66
- "tailwind-merge": ">=3.3.1"
2
+ "name": "react-native-twc",
3
+ "version": "1.2.0",
4
+ "description": "Create reusable React Native + NativeWind components with Tailwind CSS syntax.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "test": "bun test",
17
+ "build": "rm -rf dist && rolldown -c && tsc -p tsconfig.build.json",
18
+ "check-types": "tsc --noEmit",
19
+ "prepublishOnly": "bun test && bun run build"
20
+ },
21
+ "keywords": [
22
+ "react-native",
23
+ "nativewind",
24
+ "tailwind",
25
+ "css",
26
+ "components",
27
+ "styled",
28
+ "styled-components",
29
+ "expo"
30
+ ],
31
+ "author": "ldystudio",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/ldystudio/react-native-twc.git"
36
+ },
37
+ "homepage": "https://react-twc.vercel.app",
38
+ "devDependencies": {
39
+ "@biomejs/biome": "^2.3.11",
40
+ "@happy-dom/global-registrator": "^18.0.1",
41
+ "@testing-library/react": "^16.3.0",
42
+ "@types/bun": "^1.2.5",
43
+ "@types/react": "^19.1.8",
44
+ "class-variance-authority": "^0.7.1",
45
+ "happy-dom": "^18.0.1",
46
+ "prettier": "^3.6.2",
47
+ "react": "^19.1.0",
48
+ "react-dom": "^19.1.0",
49
+ "rolldown": "^1.0.0-beta.59",
50
+ "tailwind-merge": "^3.3.1",
51
+ "typescript": "^5.8.3"
52
+ },
53
+ "dependencies": {
54
+ "clsx": "^2.1.1"
55
+ },
56
+ "peerDependencies": {
57
+ "react": ">=18.0.0",
58
+ "react-native": ">=0.72.0",
59
+ "nativewind": ">=4.0.0",
60
+ "tailwind-merge": ">=3.3.1"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "react-native": {
64
+ "optional": true
67
65
  },
68
- "peerDependenciesMeta": {
69
- "react-native": {
70
- "optional": true
71
- },
72
- "nativewind": {
73
- "optional": true
74
- }
66
+ "nativewind": {
67
+ "optional": true
75
68
  }
69
+ }
76
70
  }