react-native-twc 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023-Present Greg Bergé
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,283 @@
1
+ # react-native-twc
2
+
3
+ > Create reusable React Native + NativeWind components with Tailwind CSS syntax
4
+
5
+ This project is a **React Native adaptation** of [TWC (react-twc)](https://github.com/gregberge/twc) by [Greg Bergé](https://github.com/gregberge). The original TWC library provides an elegant way to create styled React components with Tailwind CSS. This fork adapts the library specifically for React Native with NativeWind support.
6
+
7
+ ## Features
8
+
9
+ - ⚡️ **Lightweight** — only ~1KB minified
10
+ - ✨ **Full TypeScript support** with autocompletion
11
+ - 🎨 **Dynamic styling** based on props
12
+ - 🦄 **Works with any React Native component**
13
+ - 🚀 **First-class `tailwind-merge` and `cva` support**
14
+ - 📱 **Built for React Native + NativeWind**
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ # npm
20
+ npm install react-native-twc
21
+
22
+ # yarn
23
+ yarn add react-native-twc
24
+
25
+ # bun
26
+ bun add react-native-twc
27
+ ```
28
+
29
+ ### Peer Dependencies
30
+
31
+ Make sure you have the following peer dependencies installed:
32
+
33
+ ```bash
34
+ npm install react react-native nativewind tailwind-merge
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### Basic Usage
40
+
41
+ **Without `twc`:**
42
+
43
+ ```tsx
44
+ import * as React from "react";
45
+ import { View, Text, ViewProps } from "react-native";
46
+ import clsx from "clsx";
47
+
48
+ const Card = React.forwardRef<View, ViewProps>(({ className, ...props }, ref) => (
49
+ <View
50
+ ref={ref}
51
+ className={clsx("rounded-lg border bg-slate-100 p-4 shadow-sm", className)}
52
+ {...props}
53
+ />
54
+ ));
55
+ ```
56
+
57
+ **With `twc`:**
58
+
59
+ ```tsx
60
+ import { View } from "react-native";
61
+ import { twc } from "react-native-twc";
62
+
63
+ const Card = twc(View)`rounded-lg border bg-slate-100 p-4 shadow-sm`;
64
+ ```
65
+
66
+ ### Creating Styled Components
67
+
68
+ ```tsx
69
+ import { View, Text, Pressable, TextInput } from "react-native";
70
+ import { twc } from "react-native-twc";
71
+
72
+ // Basic styled components
73
+ const Container = twc(View)`flex-1 bg-white p-4`;
74
+ const Title = twc(Text)`text-2xl font-bold text-gray-900`;
75
+ const Subtitle = twc(Text)`text-lg text-gray-600`;
76
+
77
+ // Styled input
78
+ const Input = twc(TextInput)`border border-gray-300 rounded-lg px-4 py-2`;
79
+
80
+ // Styled button
81
+ const Button = twc(Pressable)`bg-blue-500 py-3 px-6 rounded-lg`;
82
+ const ButtonText = twc(Text)`text-white font-semibold text-center`;
83
+ ```
84
+
85
+ ### Dynamic Styling with Props
86
+
87
+ Use the `$` prefix for transient props that won't be passed to the underlying component:
88
+
89
+ ```tsx
90
+ import { Pressable, Text } from "react-native";
91
+ import { twc, TwcComponentProps } from "react-native-twc";
92
+
93
+ type ButtonProps = TwcComponentProps<typeof Pressable> & {
94
+ $variant?: "primary" | "secondary" | "danger";
95
+ };
96
+
97
+ const Button = twc(Pressable)<ButtonProps>((props) => [
98
+ "py-3 px-6 rounded-lg font-semibold",
99
+ {
100
+ "bg-blue-500": props.$variant === "primary",
101
+ "bg-gray-200": props.$variant === "secondary",
102
+ "bg-red-500": props.$variant === "danger",
103
+ },
104
+ ]);
105
+
106
+ // Usage
107
+ <Button $variant="primary">
108
+ <Text className="text-white">Click me</Text>
109
+ </Button>
110
+ ```
111
+
112
+ ### Using with `attrs`
113
+
114
+ Add default props to your components:
115
+
116
+ ```tsx
117
+ import { TextInput } from "react-native";
118
+ import { twc } from "react-native-twc";
119
+
120
+ const EmailInput = twc(TextInput).attrs({
121
+ keyboardType: "email-address",
122
+ autoCapitalize: "none",
123
+ placeholder: "Enter your email",
124
+ })`border border-gray-300 rounded-lg px-4 py-2`;
125
+
126
+ // Usage
127
+ <EmailInput onChangeText={(text) => console.log(text)} />
128
+ ```
129
+
130
+ ### Using with CVA (Class Variance Authority)
131
+
132
+ ```tsx
133
+ import { Pressable } from "react-native";
134
+ import { cva, VariantProps } from "class-variance-authority";
135
+ import { twc, TwcComponentProps } from "react-native-twc";
136
+
137
+ const buttonVariants = cva("rounded-lg font-semibold", {
138
+ variants: {
139
+ $intent: {
140
+ primary: "bg-blue-500 text-white",
141
+ secondary: "bg-gray-200 text-gray-800",
142
+ danger: "bg-red-500 text-white",
143
+ },
144
+ $size: {
145
+ sm: "py-1 px-2 text-sm",
146
+ md: "py-2 px-4 text-base",
147
+ lg: "py-3 px-6 text-lg",
148
+ },
149
+ },
150
+ defaultVariants: {
151
+ $intent: "primary",
152
+ $size: "md",
153
+ },
154
+ });
155
+
156
+ type ButtonProps = TwcComponentProps<typeof Pressable> &
157
+ VariantProps<typeof buttonVariants>;
158
+
159
+ const Button = twc(Pressable)<ButtonProps>(({ $intent, $size }) =>
160
+ buttonVariants({ $intent, $size })
161
+ );
162
+
163
+ // Usage
164
+ <Button $intent="danger" $size="lg">
165
+ <Text>Delete</Text>
166
+ </Button>
167
+ ```
168
+
169
+ ### Using with Tailwind Merge
170
+
171
+ Use the built-in `twx` for automatic class conflict resolution:
172
+
173
+ ```tsx
174
+ import { Text } from "react-native";
175
+ import { twx } from "react-native-twc";
176
+
177
+ const Title = twx(Text)`font-bold text-lg`;
178
+
179
+ // Later classes override earlier ones
180
+ <Title className="font-normal text-sm">Hello</Title>
181
+ // Result: "font-normal text-sm" (conflicts resolved)
182
+ ```
183
+
184
+ Or create your own instance:
185
+
186
+ ```tsx
187
+ import { twMerge } from "tailwind-merge";
188
+ import { createTwc } from "react-native-twc";
189
+
190
+ const twc = createTwc({ compose: twMerge });
191
+ ```
192
+
193
+ ### Custom Transient Props
194
+
195
+ By default, props starting with `$` are not forwarded. Customize this behavior:
196
+
197
+ ```tsx
198
+ import { View } from "react-native";
199
+ import { twc, TwcComponentProps } from "react-native-twc";
200
+
201
+ type Props = TwcComponentProps<typeof View> & { size: "sm" | "lg" };
202
+
203
+ const Box = twc(View).transientProps(["size"])<Props>((props) => ({
204
+ "w-4 h-4": props.size === "sm",
205
+ "w-8 h-8": props.size === "lg",
206
+ }));
207
+
208
+ // 'size' won't be passed to the underlying View
209
+ <Box size="lg" />
210
+ ```
211
+
212
+ ## API Reference
213
+
214
+ ### `twc(Component)`
215
+
216
+ Wraps a React Native component and returns a template function.
217
+
218
+ ```tsx
219
+ const StyledView = twc(View)`bg-white p-4`;
220
+ ```
221
+
222
+ ### `createTwc(config)`
223
+
224
+ Creates a custom TWC instance with configuration options.
225
+
226
+ ```tsx
227
+ const twc = createTwc({
228
+ compose: twMerge, // Custom class merging function
229
+ shouldForwardProp: (prop) => !prop.startsWith("_"), // Custom prop filtering
230
+ });
231
+ ```
232
+
233
+ ### `TwcComponentProps<T>`
234
+
235
+ Utility type to extract props from a TWC-wrapped component.
236
+
237
+ ```tsx
238
+ type CardProps = TwcComponentProps<typeof View>;
239
+ ```
240
+
241
+ ### `cn(...inputs)`
242
+
243
+ Utility function combining `clsx` and `tailwind-merge`.
244
+
245
+ ```tsx
246
+ import { cn } from "react-native-twc";
247
+
248
+ const className = cn("p-4", condition && "bg-blue-500", ["rounded", "shadow"]);
249
+ ```
250
+
251
+ ### `twx`
252
+
253
+ Pre-configured TWC instance using `tailwind-merge` for class conflict resolution.
254
+
255
+ ```tsx
256
+ import { twx } from "react-native-twc";
257
+
258
+ const Title = twx(Text)`font-bold`;
259
+ ```
260
+
261
+ ## Differences from Original TWC
262
+
263
+ | Feature | Original TWC | react-native-twc |
264
+ |---------|--------------|------------------|
265
+ | HTML tags (`twc.div`) | ✅ Supported | ❌ Not supported |
266
+ | `asChild` prop | ✅ Supported | ❌ Not supported |
267
+ | React Native components | ❌ Not optimized | ✅ Fully supported |
268
+ | NativeWind | ❌ Not designed for | ✅ First-class support |
269
+
270
+ ## Acknowledgements
271
+
272
+ - [TWC (react-twc)](https://github.com/gregberge/twc) by [Greg Bergé](https://github.com/gregberge) - The original inspiration and foundation for this project
273
+ - [NativeWind](https://www.nativewind.dev/) - Tailwind CSS for React Native
274
+ - [styled-components](https://styled-components.com) - Where the template literal API originated
275
+ - [tailwind-merge](https://github.com/dcastil/tailwind-merge) - Intelligent Tailwind class merging
276
+
277
+ ## License
278
+
279
+ MIT License © 2023-Present [Greg Bergé](https://github.com/gregberge)
280
+
281
+ ---
282
+
283
+ **Note:** This is a community fork adapted for React Native. For the original React (web) version, please visit [react-twc](https://github.com/gregberge/twc).
@@ -0,0 +1,101 @@
1
+ import { type ClassValue, clsx } from 'clsx';
2
+ import React from 'react';
3
+ export { clsx as cx };
4
+ type AbstractCompose = (...params: any) => any;
5
+ /**
6
+ * React Native 组件类型定义
7
+ * 支持任何接受 className 或 style prop 的 React Native 组件
8
+ */
9
+ type RNComponentType = React.ComponentType<any>;
10
+ /**
11
+ * 获取组件的 Props 类型
12
+ */
13
+ type ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never;
14
+ /**
15
+ * 结果 Props 类型
16
+ * 根据泛型参数组合最终的 props 类型
17
+ */
18
+ type ResultProps<TComponent extends RNComponentType, TProps, TExtraProps, TCompose extends AbstractCompose> = TProps extends undefined ? TExtraProps extends undefined ? Omit<ComponentProps<TComponent>, 'className'> & {
19
+ className?: Parameters<TCompose>[0];
20
+ } : Omit<ComponentProps<TComponent>, 'className'> & {
21
+ className?: Parameters<TCompose>[0];
22
+ } & TExtraProps : TProps;
23
+ /**
24
+ * 模板函数类型
25
+ * 支持模板字符串语法和函数形式
26
+ */
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
+ * 第一级模板类型
30
+ * 包含 attrs 和 transientProps 方法
31
+ */
32
+ type FirstLevelTemplate<TComponent extends RNComponentType, TCompose extends AbstractCompose, TExtraProps> = Template<TComponent, TCompose, TExtraProps> & {
33
+ /**
34
+ * 为组件添加额外的默认 props
35
+ * @example
36
+ * const TextInput = twc(RNTextInput).attrs({ keyboardType: 'email-address' })`...`
37
+ */
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>;
39
+ } & {
40
+ /**
41
+ * 防止特定 props 被转发到底层组件
42
+ * @example
43
+ * const Title = twc(Text).transientProps(['$size'])`...`
44
+ */
45
+ transientProps: (fn: string[] | ((prop: string) => boolean)) => FirstLevelTemplate<TComponent, TCompose, TExtraProps>;
46
+ };
47
+ /**
48
+ * TWC 主类型
49
+ * 只支持传入 React Native 组件,不支持字符串标签
50
+ */
51
+ type Twc<TCompose extends AbstractCompose> = <T extends RNComponentType>(component: T) => FirstLevelTemplate<T, TCompose, undefined>;
52
+ /**
53
+ * TWC 组件 Props 工具类型
54
+ * 用于获取 TWC 组件的 props 类型
55
+ */
56
+ export type TwcComponentProps<TComponent extends RNComponentType, TCompose extends AbstractCompose = typeof clsx> = ResultProps<TComponent, undefined, undefined, TCompose>;
57
+ /**
58
+ * 配置类型
59
+ */
60
+ export type Config<TCompose extends AbstractCompose> = {
61
+ /**
62
+ * className 合并函数,默认使用 clsx
63
+ * 可以替换为 tailwind-merge 等库
64
+ */
65
+ compose?: TCompose;
66
+ /**
67
+ * 判断 prop 是否应该转发到底层组件的函数
68
+ * 默认规则: prop => prop[0] !== "$" (以 $ 开头的不转发)
69
+ */
70
+ shouldForwardProp?: (prop: string) => boolean;
71
+ };
72
+ /**
73
+ * 创建 TWC 实例的工厂函数
74
+ *
75
+ * @param config - 配置选项
76
+ * @returns TWC 实例
77
+ *
78
+ * @example
79
+ * // 基础使用
80
+ * import { View, Text } from 'react-native';
81
+ * const twc = createTwc();
82
+ * const Card = twc(View)`bg-white rounded-lg p-4`;
83
+ * const Title = twc(Text)`text-xl font-bold`;
84
+ *
85
+ * @example
86
+ * // 使用 tailwind-merge
87
+ * import { twMerge } from 'tailwind-merge';
88
+ * const twc = createTwc({ compose: twMerge });
89
+ */
90
+ export declare const createTwc: <TCompose extends AbstractCompose = typeof clsx>(config?: Config<TCompose>) => Twc<TCompose>;
91
+ /**
92
+ * 默认的 TWC 实例
93
+ * 使用 clsx 作为 compose 函数
94
+ */
95
+ export declare const twc: Twc<typeof clsx>;
96
+ export declare const cn: (...inputs: ClassValue[]) => string;
97
+ /**
98
+ * 带tailwind-merge的 TWC 实例
99
+ * 使用 cn 作为 compose 函数
100
+ */
101
+ export declare const twx: Twc<(...inputs: ClassValue[]) => string>;
package/dist/index.js ADDED
@@ -0,0 +1,65 @@
1
+ // src/index.tsx
2
+ import { clsx } from "clsx";
3
+ import React from "react";
4
+ import { twMerge } from "tailwind-merge";
5
+ import { jsxDEV } from "react/jsx-dev-runtime";
6
+ function filterProps(props, shouldForwardProp) {
7
+ const filteredProps = {};
8
+ const keys = Object.keys(props);
9
+ for (let i = 0;i < keys.length; i++) {
10
+ const prop = keys[i];
11
+ if (shouldForwardProp(prop)) {
12
+ filteredProps[prop] = props[prop];
13
+ }
14
+ }
15
+ return filteredProps;
16
+ }
17
+ var createTwc = (config = {}) => {
18
+ const compose = config.compose || clsx;
19
+ const defaultShouldForwardProp = config.shouldForwardProp || ((prop) => prop[0] !== "$");
20
+ const wrap = (Component) => {
21
+ const createTemplate = (attrs, shouldForwardProp = defaultShouldForwardProp) => {
22
+ const template = (stringsOrFn, ...values) => {
23
+ const isClassFn = typeof stringsOrFn === "function";
24
+ const tplClassName = !isClassFn && String.raw({ raw: stringsOrFn }, ...values);
25
+ const ForwardedComponent = React.forwardRef((p, ref) => {
26
+ const { className: classNameProp, ...rest } = p;
27
+ const resolvedAttrs = typeof attrs === "function" ? attrs(p) : attrs ? attrs : {};
28
+ const filteredProps = filterProps({ ...resolvedAttrs, ...rest }, shouldForwardProp);
29
+ const baseClassName = isClassFn ? stringsOrFn(p) : tplClassName;
30
+ const finalClassName = typeof baseClassName === "function" ? (renderProps) => compose(baseClassName(renderProps), typeof classNameProp === "function" ? classNameProp(renderProps) : classNameProp) : compose(baseClassName, classNameProp);
31
+ const Comp = Component;
32
+ return /* @__PURE__ */ jsxDEV(Comp, {
33
+ ref,
34
+ className: finalClassName,
35
+ ...filteredProps
36
+ }, undefined, false, undefined, this);
37
+ });
38
+ ForwardedComponent.displayName = `twc(${Component.displayName || Component.name || "Component"})`;
39
+ return ForwardedComponent;
40
+ };
41
+ template.transientProps = (fnOrArray) => {
42
+ const shouldForwardProp2 = typeof fnOrArray === "function" ? (prop) => !fnOrArray(prop) : (prop) => !fnOrArray.includes(prop);
43
+ return createTemplate(attrs, shouldForwardProp2);
44
+ };
45
+ if (attrs === undefined) {
46
+ template.attrs = (attrs2) => {
47
+ return createTemplate(attrs2, shouldForwardProp);
48
+ };
49
+ }
50
+ return template;
51
+ };
52
+ return createTemplate();
53
+ };
54
+ return wrap;
55
+ };
56
+ var twc = createTwc();
57
+ var cn = (...inputs) => twMerge(clsx(inputs));
58
+ var twx = createTwc({ compose: cn });
59
+ export {
60
+ twx,
61
+ twc,
62
+ clsx as cx,
63
+ createTwc,
64
+ cn
65
+ };
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "react-native-twc",
3
+ "version": "1.0.0",
4
+ "description": "Create reusable React Native + NativeWind components with Tailwind CSS syntax.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "types": "./dist/index.d.ts",
14
+ "scripts": {
15
+ "test": "bun test",
16
+ "build": "rm -rf dist && bun run build:js && bun run build:dts",
17
+ "build:js": "bun build src/index.tsx --outdir dist --format esm --external react --external react-native --external clsx --external tailwind-merge",
18
+ "build:dts": "tsc -p tsconfig.build.json",
19
+ "check-types": "tsc --noEmit",
20
+ "publish": "bun run build && npm publish --access public"
21
+ },
22
+ "keywords": [
23
+ "react-native",
24
+ "nativewind",
25
+ "tailwind",
26
+ "css",
27
+ "components",
28
+ "styled",
29
+ "styled-components",
30
+ "expo"
31
+ ],
32
+ "author": "Greg Bergé",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/gregberge/twc.git"
37
+ },
38
+ "homepage": "https://react-twc.vercel.app",
39
+ "funding": {
40
+ "type": "github",
41
+ "url": "https://github.com/sponsors/gregberge"
42
+ },
43
+ "devDependencies": {
44
+ "@eslint/eslintrc": "^3.3.1",
45
+ "@eslint/js": "^9.30.0",
46
+ "@happy-dom/global-registrator": "^18.0.1",
47
+ "@testing-library/react": "^16.3.0",
48
+ "@types/bun": "^1.2.5",
49
+ "@types/react": "^19.1.8",
50
+ "@typescript-eslint/eslint-plugin": "^8.35.0",
51
+ "@typescript-eslint/parser": "^8.35.0",
52
+ "class-variance-authority": "^0.7.1",
53
+ "eslint": "^9.30.0",
54
+ "happy-dom": "^18.0.1",
55
+ "prettier": "^3.6.2",
56
+ "react": "^19.1.0",
57
+ "react-dom": "^19.1.0",
58
+ "tailwind-merge": "^3.3.1",
59
+ "typescript": "^5.8.3",
60
+ "typescript-eslint": "^8.35.0",
61
+ "ultracite": "7.0.10"
62
+ },
63
+ "dependencies": {
64
+ "clsx": "^2.1.1"
65
+ },
66
+ "peerDependencies": {
67
+ "react": ">=18.0.0",
68
+ "react-native": ">=0.72.0",
69
+ "nativewind": ">=4.0.0",
70
+ "tailwind-merge": ">=3.3.1"
71
+ },
72
+ "peerDependenciesMeta": {
73
+ "react-native": {
74
+ "optional": true
75
+ },
76
+ "nativewind": {
77
+ "optional": true
78
+ }
79
+ }
80
+ }