rn-cn-ui 1.0.0 → 1.0.1
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 +41 -68
- package/package.json +1 -1
- package/registry.json +35 -0
- package/src/components/ui/accordion.tsx +109 -0
- package/src/components/ui/aspect-ratio.tsx +21 -0
- package/src/components/ui/dialog.tsx +100 -0
- package/src/components/ui/select.tsx +143 -0
- package/src/components/ui/tabs.tsx +174 -0
- package/src/components/ui/toggle-group.tsx +87 -0
- package/src/components/ui/toggle.tsx +58 -0
- package/src/index.ts +7 -0
package/README.md
CHANGED
|
@@ -1,90 +1,63 @@
|
|
|
1
|
-
#
|
|
1
|
+
# rn-cn-ui
|
|
2
2
|
|
|
3
3
|
A high-quality, open-source UI library for React Native, inspired by shadcn/ui.
|
|
4
|
+
**This is a CLI tool** that copies the component code into your project, allowing you to customize it fully.
|
|
4
5
|
|
|
5
|
-
##
|
|
6
|
+
## Prerequisites
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **Class Merging**: Uses `clsx` and `tailwind-merge` for easy styling overrides.
|
|
10
|
-
- **NativeWind**: Built on top of NativeWind for Tailwind CSS in React Native.
|
|
11
|
-
|
|
12
|
-
## Installation
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
npm install rn-cn-ui
|
|
16
|
-
```
|
|
8
|
+
1. **React Native** project (Expo or CLI).
|
|
9
|
+
2. **NativeWind** and **Tailwind CSS** configured.
|
|
17
10
|
|
|
18
11
|
## Usage
|
|
19
12
|
|
|
20
|
-
|
|
13
|
+
You do not install this library as a dependency. Instead, you use the CLI to add components to your project.
|
|
21
14
|
|
|
22
|
-
|
|
23
|
-
import { Button } from "rn-cn-ui"
|
|
15
|
+
### Adding a Component
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
<Button variant="destructive" label="Delete" />
|
|
27
|
-
<Button variant="outline" label="Cancel" />
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
### Text
|
|
31
|
-
|
|
32
|
-
```tsx
|
|
33
|
-
import { Text } from "rn-cn-ui"
|
|
34
|
-
|
|
35
|
-
<Text variant="h1">Heading 1</Text>
|
|
36
|
-
<Text variant="lead">This is a lead text.</Text>
|
|
37
|
-
```
|
|
17
|
+
Run the following command to add a component (e.g., `button`) to your project:
|
|
38
18
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
```tsx
|
|
42
|
-
import { Input, Label } from "rn-cn-ui"
|
|
43
|
-
|
|
44
|
-
<Label>Email</Label>
|
|
45
|
-
<Input placeholder="Enter your email" />
|
|
19
|
+
```bash
|
|
20
|
+
npx rn-cn-ui add button
|
|
46
21
|
```
|
|
47
22
|
|
|
48
|
-
|
|
23
|
+
This will:
|
|
24
|
+
1. Download `button.tsx` to your `src/components/ui/` directory.
|
|
25
|
+
2. Install necessary dependencies (like `class-variance-authority`, `clsx`, `tailwind-merge`).
|
|
49
26
|
|
|
50
|
-
|
|
51
|
-
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, Button } from "rn-cn-ui"
|
|
52
|
-
|
|
53
|
-
<Card>
|
|
54
|
-
<CardHeader>
|
|
55
|
-
<CardTitle>Card Title</CardTitle>
|
|
56
|
-
<CardDescription>Card Description</CardDescription>
|
|
57
|
-
</CardHeader>
|
|
58
|
-
<CardContent>
|
|
59
|
-
<Text>Content goes here</Text>
|
|
60
|
-
</CardContent>
|
|
61
|
-
<CardFooter>
|
|
62
|
-
<Button label="Action" />
|
|
63
|
-
</CardFooter>
|
|
64
|
-
</Card>
|
|
65
|
-
```
|
|
27
|
+
### Using the Component
|
|
66
28
|
|
|
67
|
-
|
|
29
|
+
Once added, import the component from your local directory:
|
|
68
30
|
|
|
69
31
|
```tsx
|
|
70
|
-
import {
|
|
32
|
+
import { Button } from "./src/components/ui/button"
|
|
71
33
|
|
|
72
|
-
|
|
73
|
-
|
|
34
|
+
export default function App() {
|
|
35
|
+
return (
|
|
36
|
+
<Button label="Click me" onPress={() => console.log("Pressed")} />
|
|
37
|
+
)
|
|
38
|
+
}
|
|
74
39
|
```
|
|
75
40
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
41
|
+
## Available Components
|
|
42
|
+
|
|
43
|
+
You can add any of the following components:
|
|
44
|
+
|
|
45
|
+
- `button`
|
|
46
|
+
- `text`
|
|
47
|
+
- `input`
|
|
48
|
+
- `card`
|
|
49
|
+
- `badge`
|
|
50
|
+
- `avatar`
|
|
51
|
+
- `label`
|
|
52
|
+
- `separator`
|
|
53
|
+
- `skeleton`
|
|
54
|
+
- `spinner`
|
|
55
|
+
- `switch`
|
|
56
|
+
- `checkbox`
|
|
57
|
+
- `radio-group`
|
|
58
|
+
- `textarea`
|
|
59
|
+
- `alert`
|
|
60
|
+
- `progress`
|
|
88
61
|
|
|
89
62
|
## Configuration
|
|
90
63
|
|
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -78,5 +78,40 @@
|
|
|
78
78
|
"name": "progress",
|
|
79
79
|
"dependencies": ["clsx", "tailwind-merge"],
|
|
80
80
|
"files": ["src/components/ui/progress.tsx"]
|
|
81
|
+
},
|
|
82
|
+
"accordion": {
|
|
83
|
+
"name": "accordion",
|
|
84
|
+
"dependencies": ["lucide-react-native", "clsx", "tailwind-merge"],
|
|
85
|
+
"files": ["src/components/ui/accordion.tsx"]
|
|
86
|
+
},
|
|
87
|
+
"aspect-ratio": {
|
|
88
|
+
"name": "aspect-ratio",
|
|
89
|
+
"dependencies": ["clsx", "tailwind-merge"],
|
|
90
|
+
"files": ["src/components/ui/aspect-ratio.tsx"]
|
|
91
|
+
},
|
|
92
|
+
"dialog": {
|
|
93
|
+
"name": "dialog",
|
|
94
|
+
"dependencies": ["lucide-react-native", "clsx", "tailwind-merge"],
|
|
95
|
+
"files": ["src/components/ui/dialog.tsx"]
|
|
96
|
+
},
|
|
97
|
+
"select": {
|
|
98
|
+
"name": "select",
|
|
99
|
+
"dependencies": ["lucide-react-native", "clsx", "tailwind-merge"],
|
|
100
|
+
"files": ["src/components/ui/select.tsx"]
|
|
101
|
+
},
|
|
102
|
+
"tabs": {
|
|
103
|
+
"name": "tabs",
|
|
104
|
+
"dependencies": ["clsx", "tailwind-merge"],
|
|
105
|
+
"files": ["src/components/ui/tabs.tsx"]
|
|
106
|
+
},
|
|
107
|
+
"toggle": {
|
|
108
|
+
"name": "toggle",
|
|
109
|
+
"dependencies": ["class-variance-authority", "clsx", "tailwind-merge"],
|
|
110
|
+
"files": ["src/components/ui/toggle.tsx"]
|
|
111
|
+
},
|
|
112
|
+
"toggle-group": {
|
|
113
|
+
"name": "toggle-group",
|
|
114
|
+
"dependencies": ["class-variance-authority", "clsx", "tailwind-merge"],
|
|
115
|
+
"files": ["src/components/ui/toggle-group.tsx", "src/components/ui/toggle.tsx"]
|
|
81
116
|
}
|
|
82
117
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { View, Pressable, Text, LayoutAnimation, Platform, UIManager } from "react-native"
|
|
3
|
+
import { ChevronDown } from "lucide-react-native"
|
|
4
|
+
import { cn } from "../../lib/utils"
|
|
5
|
+
|
|
6
|
+
if (
|
|
7
|
+
Platform.OS === "android" &&
|
|
8
|
+
UIManager.setLayoutAnimationEnabledExperimental
|
|
9
|
+
) {
|
|
10
|
+
UIManager.setLayoutAnimationEnabledExperimental(true)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const Accordion = React.forwardRef<
|
|
14
|
+
React.ElementRef<typeof View>,
|
|
15
|
+
React.ComponentPropsWithoutRef<typeof View> & {
|
|
16
|
+
type?: "single" | "multiple"
|
|
17
|
+
collapsible?: boolean
|
|
18
|
+
defaultValue?: string | string[]
|
|
19
|
+
onValueChange?: (value: string | string[]) => void
|
|
20
|
+
}
|
|
21
|
+
>(({ className, type = "single", collapsible = false, defaultValue, onValueChange, children, ...props }, ref) => {
|
|
22
|
+
const [value, setValue] = React.useState<string | string[]>(defaultValue || (type === "multiple" ? [] : ""))
|
|
23
|
+
|
|
24
|
+
const handleValueChange = (itemValue: string) => {
|
|
25
|
+
if (type === "single") {
|
|
26
|
+
const newValue = value === itemValue && collapsible ? "" : itemValue
|
|
27
|
+
setValue(newValue)
|
|
28
|
+
onValueChange?.(newValue)
|
|
29
|
+
} else {
|
|
30
|
+
const currentValues = Array.isArray(value) ? value : []
|
|
31
|
+
const newValue = currentValues.includes(itemValue)
|
|
32
|
+
? currentValues.filter((v) => v !== itemValue)
|
|
33
|
+
: [...currentValues, itemValue]
|
|
34
|
+
setValue(newValue)
|
|
35
|
+
onValueChange?.(newValue)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<View ref={ref} className={cn("gap-2", className)} {...props}>
|
|
41
|
+
{React.Children.map(children, (child) => {
|
|
42
|
+
if (React.isValidElement(child)) {
|
|
43
|
+
return React.cloneElement(child, {
|
|
44
|
+
// @ts-ignore
|
|
45
|
+
expanded: Array.isArray(value) ? value.includes(child.props.value) : value === child.props.value,
|
|
46
|
+
// @ts-ignore
|
|
47
|
+
onPress: () => handleValueChange(child.props.value),
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
return child
|
|
51
|
+
})}
|
|
52
|
+
</View>
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
Accordion.displayName = "Accordion"
|
|
56
|
+
|
|
57
|
+
const AccordionItem = React.forwardRef<
|
|
58
|
+
React.ElementRef<typeof View>,
|
|
59
|
+
React.ComponentPropsWithoutRef<typeof View> & { value: string }
|
|
60
|
+
>(({ className, ...props }, ref) => (
|
|
61
|
+
<View ref={ref} className={cn("border-b border-border", className)} {...props} />
|
|
62
|
+
))
|
|
63
|
+
AccordionItem.displayName = "AccordionItem"
|
|
64
|
+
|
|
65
|
+
const AccordionTrigger = React.forwardRef<
|
|
66
|
+
React.ElementRef<typeof Pressable>,
|
|
67
|
+
React.ComponentPropsWithoutRef<typeof Pressable> & { expanded?: boolean }
|
|
68
|
+
>(({ className, children, expanded, onPress, ...props }, ref) => (
|
|
69
|
+
<Pressable
|
|
70
|
+
ref={ref}
|
|
71
|
+
onPress={(e) => {
|
|
72
|
+
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
|
73
|
+
onPress?.(e)
|
|
74
|
+
}}
|
|
75
|
+
className={cn(
|
|
76
|
+
"flex-row items-center justify-between py-4 font-medium transition-all",
|
|
77
|
+
className
|
|
78
|
+
)}
|
|
79
|
+
{...props}
|
|
80
|
+
>
|
|
81
|
+
{typeof children === 'string' ? (
|
|
82
|
+
<Text className="text-sm font-medium text-foreground">{children}</Text>
|
|
83
|
+
) : children}
|
|
84
|
+
<ChevronDown
|
|
85
|
+
size={18}
|
|
86
|
+
className={cn("text-muted-foreground transition-transform duration-200", expanded ? "rotate-180" : "")}
|
|
87
|
+
/>
|
|
88
|
+
</Pressable>
|
|
89
|
+
))
|
|
90
|
+
AccordionTrigger.displayName = "AccordionTrigger"
|
|
91
|
+
|
|
92
|
+
const AccordionContent = React.forwardRef<
|
|
93
|
+
React.ElementRef<typeof View>,
|
|
94
|
+
React.ComponentPropsWithoutRef<typeof View> & { expanded?: boolean }
|
|
95
|
+
>(({ className, children, expanded, ...props }, ref) => {
|
|
96
|
+
if (!expanded) return null
|
|
97
|
+
return (
|
|
98
|
+
<View
|
|
99
|
+
ref={ref}
|
|
100
|
+
className={cn("overflow-hidden text-sm transition-all pb-4", className)}
|
|
101
|
+
{...props}
|
|
102
|
+
>
|
|
103
|
+
<View className="pt-0 pb-4">{children}</View>
|
|
104
|
+
</View>
|
|
105
|
+
)
|
|
106
|
+
})
|
|
107
|
+
AccordionContent.displayName = "AccordionContent"
|
|
108
|
+
|
|
109
|
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { View } from "react-native"
|
|
3
|
+
import { cn } from "../../lib/utils"
|
|
4
|
+
|
|
5
|
+
interface AspectRatioProps extends React.ComponentPropsWithoutRef<typeof View> {
|
|
6
|
+
ratio?: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const AspectRatio = React.forwardRef<React.ElementRef<typeof View>, AspectRatioProps>(
|
|
10
|
+
({ className, ratio = 1, style, ...props }, ref) => (
|
|
11
|
+
<View
|
|
12
|
+
ref={ref}
|
|
13
|
+
style={[style, { aspectRatio: ratio }]}
|
|
14
|
+
className={cn("w-full", className)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
AspectRatio.displayName = "AspectRatio"
|
|
20
|
+
|
|
21
|
+
export { AspectRatio }
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Modal, View, Pressable, Text } from "react-native"
|
|
3
|
+
import { X } from "lucide-react-native"
|
|
4
|
+
import { cn } from "../../lib/utils"
|
|
5
|
+
|
|
6
|
+
const Dialog = React.forwardRef<
|
|
7
|
+
React.ElementRef<typeof Modal>,
|
|
8
|
+
React.ComponentPropsWithoutRef<typeof Modal> & {
|
|
9
|
+
open?: boolean
|
|
10
|
+
onOpenChange?: (open: boolean) => void
|
|
11
|
+
}
|
|
12
|
+
>(({ className, children, open, onOpenChange, ...props }, ref) => (
|
|
13
|
+
<Modal
|
|
14
|
+
ref={ref}
|
|
15
|
+
transparent
|
|
16
|
+
animationType="fade"
|
|
17
|
+
visible={open}
|
|
18
|
+
onRequestClose={() => onOpenChange?.(false)}
|
|
19
|
+
{...props}
|
|
20
|
+
>
|
|
21
|
+
<View className="flex-1 items-center justify-center bg-black/50 p-4">
|
|
22
|
+
<Pressable className="absolute inset-0" onPress={() => onOpenChange?.(false)} />
|
|
23
|
+
<View
|
|
24
|
+
className={cn(
|
|
25
|
+
"w-full max-w-lg gap-4 rounded-lg border border-border bg-background p-6 shadow-lg",
|
|
26
|
+
className
|
|
27
|
+
)}
|
|
28
|
+
>
|
|
29
|
+
{children}
|
|
30
|
+
<Pressable
|
|
31
|
+
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none"
|
|
32
|
+
onPress={() => onOpenChange?.(false)}
|
|
33
|
+
>
|
|
34
|
+
<X size={18} className="text-muted-foreground" />
|
|
35
|
+
</Pressable>
|
|
36
|
+
</View>
|
|
37
|
+
</View>
|
|
38
|
+
</Modal>
|
|
39
|
+
))
|
|
40
|
+
Dialog.displayName = "Dialog"
|
|
41
|
+
|
|
42
|
+
const DialogHeader = ({
|
|
43
|
+
className,
|
|
44
|
+
...props
|
|
45
|
+
}: React.ComponentPropsWithoutRef<typeof View>) => (
|
|
46
|
+
<View
|
|
47
|
+
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
)
|
|
51
|
+
DialogHeader.displayName = "DialogHeader"
|
|
52
|
+
|
|
53
|
+
const DialogFooter = ({
|
|
54
|
+
className,
|
|
55
|
+
...props
|
|
56
|
+
}: React.ComponentPropsWithoutRef<typeof View>) => (
|
|
57
|
+
<View
|
|
58
|
+
className={cn(
|
|
59
|
+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
60
|
+
className
|
|
61
|
+
)}
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
)
|
|
65
|
+
DialogFooter.displayName = "DialogFooter"
|
|
66
|
+
|
|
67
|
+
const DialogTitle = React.forwardRef<
|
|
68
|
+
React.ElementRef<typeof Text>,
|
|
69
|
+
React.ComponentPropsWithoutRef<typeof Text>
|
|
70
|
+
>(({ className, ...props }, ref) => (
|
|
71
|
+
<Text
|
|
72
|
+
ref={ref}
|
|
73
|
+
className={cn(
|
|
74
|
+
"text-lg font-semibold leading-none tracking-tight text-foreground",
|
|
75
|
+
className
|
|
76
|
+
)}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
))
|
|
80
|
+
DialogTitle.displayName = "DialogTitle"
|
|
81
|
+
|
|
82
|
+
const DialogDescription = React.forwardRef<
|
|
83
|
+
React.ElementRef<typeof Text>,
|
|
84
|
+
React.ComponentPropsWithoutRef<typeof Text>
|
|
85
|
+
>(({ className, ...props }, ref) => (
|
|
86
|
+
<Text
|
|
87
|
+
ref={ref}
|
|
88
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
89
|
+
{...props}
|
|
90
|
+
/>
|
|
91
|
+
))
|
|
92
|
+
DialogDescription.displayName = "DialogDescription"
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
Dialog,
|
|
96
|
+
DialogHeader,
|
|
97
|
+
DialogFooter,
|
|
98
|
+
DialogTitle,
|
|
99
|
+
DialogDescription,
|
|
100
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { View, Pressable, Text, Modal, FlatList } from "react-native"
|
|
3
|
+
import { Check, ChevronDown } from "lucide-react-native"
|
|
4
|
+
import { cn } from "../../lib/utils"
|
|
5
|
+
|
|
6
|
+
interface SelectContextValue {
|
|
7
|
+
value?: string
|
|
8
|
+
onValueChange?: (value: string) => void
|
|
9
|
+
open: boolean
|
|
10
|
+
setOpen: (open: boolean) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const SelectContext = React.createContext<SelectContextValue>({
|
|
14
|
+
open: false,
|
|
15
|
+
setOpen: () => {},
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const Select = ({
|
|
19
|
+
value,
|
|
20
|
+
onValueChange,
|
|
21
|
+
children,
|
|
22
|
+
}: {
|
|
23
|
+
value?: string
|
|
24
|
+
onValueChange?: (value: string) => void
|
|
25
|
+
children: React.ReactNode
|
|
26
|
+
}) => {
|
|
27
|
+
const [open, setOpen] = React.useState(false)
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<SelectContext.Provider value={{ value, onValueChange, open, setOpen }}>
|
|
31
|
+
{children}
|
|
32
|
+
</SelectContext.Provider>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SelectTrigger = React.forwardRef<
|
|
37
|
+
React.ElementRef<typeof Pressable>,
|
|
38
|
+
React.ComponentPropsWithoutRef<typeof Pressable>
|
|
39
|
+
>(({ className, children, ...props }, ref) => {
|
|
40
|
+
const { setOpen } = React.useContext(SelectContext)
|
|
41
|
+
return (
|
|
42
|
+
<Pressable
|
|
43
|
+
ref={ref}
|
|
44
|
+
onPress={() => setOpen(true)}
|
|
45
|
+
className={cn(
|
|
46
|
+
"flex h-10 w-full flex-row items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
47
|
+
className
|
|
48
|
+
)}
|
|
49
|
+
{...props}
|
|
50
|
+
>
|
|
51
|
+
{children}
|
|
52
|
+
<ChevronDown size={16} className="opacity-50" />
|
|
53
|
+
</Pressable>
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
SelectTrigger.displayName = "SelectTrigger"
|
|
57
|
+
|
|
58
|
+
const SelectValue = React.forwardRef<
|
|
59
|
+
React.ElementRef<typeof Text>,
|
|
60
|
+
React.ComponentPropsWithoutRef<typeof Text> & { placeholder?: string }
|
|
61
|
+
>(({ className, placeholder, ...props }, ref) => {
|
|
62
|
+
const { value } = React.useContext(SelectContext)
|
|
63
|
+
return (
|
|
64
|
+
<Text
|
|
65
|
+
ref={ref}
|
|
66
|
+
className={cn("text-sm text-foreground", !value && "text-muted-foreground", className)}
|
|
67
|
+
{...props}
|
|
68
|
+
>
|
|
69
|
+
{value || placeholder}
|
|
70
|
+
</Text>
|
|
71
|
+
)
|
|
72
|
+
})
|
|
73
|
+
SelectValue.displayName = "SelectValue"
|
|
74
|
+
|
|
75
|
+
const SelectContent = ({
|
|
76
|
+
className,
|
|
77
|
+
children,
|
|
78
|
+
...props
|
|
79
|
+
}: React.ComponentPropsWithoutRef<typeof View>) => {
|
|
80
|
+
const { open, setOpen } = React.useContext(SelectContext)
|
|
81
|
+
|
|
82
|
+
if (!open) return null
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Modal
|
|
86
|
+
transparent
|
|
87
|
+
animationType="fade"
|
|
88
|
+
visible={open}
|
|
89
|
+
onRequestClose={() => setOpen(false)}
|
|
90
|
+
>
|
|
91
|
+
<Pressable className="flex-1 bg-black/50" onPress={() => setOpen(false)}>
|
|
92
|
+
<View className="flex-1 justify-center p-4">
|
|
93
|
+
<View
|
|
94
|
+
className={cn(
|
|
95
|
+
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md animate-in fade-in-80",
|
|
96
|
+
className
|
|
97
|
+
)}
|
|
98
|
+
{...props}
|
|
99
|
+
>
|
|
100
|
+
<View className="p-1">
|
|
101
|
+
{children}
|
|
102
|
+
</View>
|
|
103
|
+
</View>
|
|
104
|
+
</View>
|
|
105
|
+
</Pressable>
|
|
106
|
+
</Modal>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
SelectContent.displayName = "SelectContent"
|
|
110
|
+
|
|
111
|
+
const SelectItem = React.forwardRef<
|
|
112
|
+
React.ElementRef<typeof Pressable>,
|
|
113
|
+
React.ComponentPropsWithoutRef<typeof Pressable> & { value: string }
|
|
114
|
+
>(({ className, children, value, ...props }, ref) => {
|
|
115
|
+
const { value: selectedValue, onValueChange, setOpen } = React.useContext(SelectContext)
|
|
116
|
+
const isSelected = selectedValue === value
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<Pressable
|
|
120
|
+
ref={ref}
|
|
121
|
+
onPress={() => {
|
|
122
|
+
onValueChange?.(value)
|
|
123
|
+
setOpen(false)
|
|
124
|
+
}}
|
|
125
|
+
className={cn(
|
|
126
|
+
"relative flex w-full cursor-default select-none flex-row items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
127
|
+
isSelected && "bg-accent",
|
|
128
|
+
className
|
|
129
|
+
)}
|
|
130
|
+
{...props}
|
|
131
|
+
>
|
|
132
|
+
{isSelected && (
|
|
133
|
+
<View className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
134
|
+
<Check size={14} className="text-foreground" />
|
|
135
|
+
</View>
|
|
136
|
+
)}
|
|
137
|
+
<Text className="text-popover-foreground">{children}</Text>
|
|
138
|
+
</Pressable>
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
SelectItem.displayName = "SelectItem"
|
|
142
|
+
|
|
143
|
+
export { Select, SelectTrigger, SelectValue, SelectContent, SelectItem }
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { View, Pressable, Text } from "react-native"
|
|
3
|
+
import { cn } from "../../lib/utils"
|
|
4
|
+
|
|
5
|
+
const Tabs = React.forwardRef<
|
|
6
|
+
React.ElementRef<typeof View>,
|
|
7
|
+
React.ComponentPropsWithoutRef<typeof View> & {
|
|
8
|
+
value: string
|
|
9
|
+
onValueChange: (value: string) => void
|
|
10
|
+
}
|
|
11
|
+
>(({ className, value, onValueChange, children, ...props }, ref) => (
|
|
12
|
+
<View ref={ref} className={cn("", className)} {...props}>
|
|
13
|
+
{React.Children.map(children, (child) => {
|
|
14
|
+
if (React.isValidElement(child)) {
|
|
15
|
+
return React.cloneElement(child, {
|
|
16
|
+
// @ts-ignore
|
|
17
|
+
value,
|
|
18
|
+
// @ts-ignore
|
|
19
|
+
onValueChange,
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
return child
|
|
23
|
+
})}
|
|
24
|
+
</View>
|
|
25
|
+
))
|
|
26
|
+
Tabs.displayName = "Tabs"
|
|
27
|
+
|
|
28
|
+
const TabsList = React.forwardRef<
|
|
29
|
+
React.ElementRef<typeof View>,
|
|
30
|
+
React.ComponentPropsWithoutRef<typeof View>
|
|
31
|
+
>(({ className, ...props }, ref) => (
|
|
32
|
+
<View
|
|
33
|
+
ref={ref}
|
|
34
|
+
className={cn(
|
|
35
|
+
"inline-flex h-10 flex-row items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
))
|
|
41
|
+
TabsList.displayName = "TabsList"
|
|
42
|
+
|
|
43
|
+
const TabsTrigger = React.forwardRef<
|
|
44
|
+
React.ElementRef<typeof Pressable>,
|
|
45
|
+
React.ComponentPropsWithoutRef<typeof Pressable> & {
|
|
46
|
+
value: string
|
|
47
|
+
activeValue?: string
|
|
48
|
+
onValueChange?: (value: string) => void
|
|
49
|
+
}
|
|
50
|
+
>(({ className, value, activeValue, onValueChange, children, ...props }, ref) => {
|
|
51
|
+
// Note: activeValue and onValueChange are injected by parent Tabs/TabsList if we structured it that way,
|
|
52
|
+
// but here we rely on the user passing context or we can use a Context API.
|
|
53
|
+
// For simplicity in this "copy-paste" component, let's assume Tabs injects props or we use Context.
|
|
54
|
+
// Let's switch to Context for cleaner API.
|
|
55
|
+
return (
|
|
56
|
+
<Pressable
|
|
57
|
+
ref={ref}
|
|
58
|
+
onPress={() => onValueChange?.(value)}
|
|
59
|
+
className={cn(
|
|
60
|
+
"inline-flex flex-1 items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
61
|
+
activeValue === value && "bg-background text-foreground shadow-sm",
|
|
62
|
+
className
|
|
63
|
+
)}
|
|
64
|
+
{...props}
|
|
65
|
+
>
|
|
66
|
+
<Text className={cn("text-sm font-medium", activeValue === value ? "text-foreground" : "text-muted-foreground")}>
|
|
67
|
+
{children}
|
|
68
|
+
</Text>
|
|
69
|
+
</Pressable>
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
TabsTrigger.displayName = "TabsTrigger"
|
|
73
|
+
|
|
74
|
+
const TabsContent = React.forwardRef<
|
|
75
|
+
React.ElementRef<typeof View>,
|
|
76
|
+
React.ComponentPropsWithoutRef<typeof View> & {
|
|
77
|
+
value: string
|
|
78
|
+
activeValue?: string
|
|
79
|
+
}
|
|
80
|
+
>(({ className, value, activeValue, children, ...props }, ref) => {
|
|
81
|
+
if (value !== activeValue) return null
|
|
82
|
+
return (
|
|
83
|
+
<View
|
|
84
|
+
ref={ref}
|
|
85
|
+
className={cn(
|
|
86
|
+
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
87
|
+
className
|
|
88
|
+
)}
|
|
89
|
+
{...props}
|
|
90
|
+
>
|
|
91
|
+
{children}
|
|
92
|
+
</View>
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
TabsContent.displayName = "TabsContent"
|
|
96
|
+
|
|
97
|
+
// Re-implementing Tabs with Context to make it work properly
|
|
98
|
+
const TabsContext = React.createContext<{
|
|
99
|
+
value: string
|
|
100
|
+
onValueChange: (value: string) => void
|
|
101
|
+
}>({ value: "", onValueChange: () => {} })
|
|
102
|
+
|
|
103
|
+
const TabsRoot = React.forwardRef<
|
|
104
|
+
React.ElementRef<typeof View>,
|
|
105
|
+
React.ComponentPropsWithoutRef<typeof View> & {
|
|
106
|
+
defaultValue: string
|
|
107
|
+
onValueChange?: (value: string) => void
|
|
108
|
+
}
|
|
109
|
+
>(({ className, defaultValue, onValueChange, children, ...props }, ref) => {
|
|
110
|
+
const [value, setValue] = React.useState(defaultValue)
|
|
111
|
+
const handleValueChange = (v: string) => {
|
|
112
|
+
setValue(v)
|
|
113
|
+
onValueChange?.(v)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<TabsContext.Provider value={{ value, onValueChange: handleValueChange }}>
|
|
118
|
+
<View ref={ref} className={cn("", className)} {...props}>
|
|
119
|
+
{children}
|
|
120
|
+
</View>
|
|
121
|
+
</TabsContext.Provider>
|
|
122
|
+
)
|
|
123
|
+
})
|
|
124
|
+
TabsRoot.displayName = "Tabs"
|
|
125
|
+
|
|
126
|
+
const TabsTriggerWithContext = React.forwardRef<
|
|
127
|
+
React.ElementRef<typeof Pressable>,
|
|
128
|
+
React.ComponentPropsWithoutRef<typeof Pressable> & { value: string }
|
|
129
|
+
>(({ className, value, children, ...props }, ref) => {
|
|
130
|
+
const context = React.useContext(TabsContext)
|
|
131
|
+
const isActive = context.value === value
|
|
132
|
+
return (
|
|
133
|
+
<Pressable
|
|
134
|
+
ref={ref}
|
|
135
|
+
onPress={() => context.onValueChange(value)}
|
|
136
|
+
className={cn(
|
|
137
|
+
"inline-flex flex-1 items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
138
|
+
isActive && "bg-background text-foreground shadow-sm",
|
|
139
|
+
className
|
|
140
|
+
)}
|
|
141
|
+
{...props}
|
|
142
|
+
>
|
|
143
|
+
{typeof children === 'string' ? (
|
|
144
|
+
<Text className={cn("text-sm font-medium", isActive ? "text-foreground" : "text-muted-foreground")}>
|
|
145
|
+
{children}
|
|
146
|
+
</Text>
|
|
147
|
+
) : children}
|
|
148
|
+
</Pressable>
|
|
149
|
+
)
|
|
150
|
+
})
|
|
151
|
+
TabsTriggerWithContext.displayName = "TabsTrigger"
|
|
152
|
+
|
|
153
|
+
const TabsContentWithContext = React.forwardRef<
|
|
154
|
+
React.ElementRef<typeof View>,
|
|
155
|
+
React.ComponentPropsWithoutRef<typeof View> & { value: string }
|
|
156
|
+
>(({ className, value, children, ...props }, ref) => {
|
|
157
|
+
const context = React.useContext(TabsContext)
|
|
158
|
+
if (context.value !== value) return null
|
|
159
|
+
return (
|
|
160
|
+
<View
|
|
161
|
+
ref={ref}
|
|
162
|
+
className={cn(
|
|
163
|
+
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
164
|
+
className
|
|
165
|
+
)}
|
|
166
|
+
{...props}
|
|
167
|
+
>
|
|
168
|
+
{children}
|
|
169
|
+
</View>
|
|
170
|
+
)
|
|
171
|
+
})
|
|
172
|
+
TabsContentWithContext.displayName = "TabsContent"
|
|
173
|
+
|
|
174
|
+
export { TabsRoot as Tabs, TabsList, TabsTriggerWithContext as TabsTrigger, TabsContentWithContext as TabsContent }
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { View } from "react-native"
|
|
3
|
+
import { cn } from "../../lib/utils"
|
|
4
|
+
import { toggleVariants } from "./toggle"
|
|
5
|
+
import { type VariantProps } from "class-variance-authority"
|
|
6
|
+
|
|
7
|
+
const ToggleGroupContext = React.createContext<{
|
|
8
|
+
size?: VariantProps<typeof toggleVariants>["size"]
|
|
9
|
+
variant?: VariantProps<typeof toggleVariants>["variant"]
|
|
10
|
+
value: string | string[]
|
|
11
|
+
onValueChange: (value: string) => void
|
|
12
|
+
type: "single" | "multiple"
|
|
13
|
+
}>({
|
|
14
|
+
size: "default",
|
|
15
|
+
variant: "default",
|
|
16
|
+
value: "",
|
|
17
|
+
onValueChange: () => {},
|
|
18
|
+
type: "single",
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const ToggleGroup = React.forwardRef<
|
|
22
|
+
React.ElementRef<typeof View>,
|
|
23
|
+
React.ComponentPropsWithoutRef<typeof View> &
|
|
24
|
+
VariantProps<typeof toggleVariants> & {
|
|
25
|
+
type: "single" | "multiple"
|
|
26
|
+
value?: string | string[]
|
|
27
|
+
onValueChange?: (value: string | string[]) => void
|
|
28
|
+
}
|
|
29
|
+
>(({ className, variant, size, children, type, value: valueProp, onValueChange, ...props }, ref) => {
|
|
30
|
+
const [value, setValue] = React.useState<string | string[]>(valueProp || (type === "multiple" ? [] : ""))
|
|
31
|
+
|
|
32
|
+
const handleValueChange = (itemValue: string) => {
|
|
33
|
+
let newValue: string | string[]
|
|
34
|
+
if (type === "single") {
|
|
35
|
+
newValue = itemValue === value ? "" : itemValue
|
|
36
|
+
} else {
|
|
37
|
+
const currentValues = Array.isArray(value) ? value : []
|
|
38
|
+
newValue = currentValues.includes(itemValue)
|
|
39
|
+
? currentValues.filter((v) => v !== itemValue)
|
|
40
|
+
: [...currentValues, itemValue]
|
|
41
|
+
}
|
|
42
|
+
setValue(newValue)
|
|
43
|
+
onValueChange?.(newValue)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<ToggleGroupContext.Provider value={{ variant, size, value, onValueChange: handleValueChange, type }}>
|
|
48
|
+
<View
|
|
49
|
+
ref={ref}
|
|
50
|
+
className={cn("flex flex-row items-center justify-center gap-1", className)}
|
|
51
|
+
{...props}
|
|
52
|
+
>
|
|
53
|
+
{children}
|
|
54
|
+
</View>
|
|
55
|
+
</ToggleGroupContext.Provider>
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
ToggleGroup.displayName = "ToggleGroup"
|
|
59
|
+
|
|
60
|
+
import { Toggle } from "./toggle"
|
|
61
|
+
|
|
62
|
+
const ToggleGroupItem = React.forwardRef<
|
|
63
|
+
React.ElementRef<typeof Toggle>,
|
|
64
|
+
React.ComponentPropsWithoutRef<typeof Toggle> & { value: string }
|
|
65
|
+
>(({ className, children, value, ...props }, ref) => {
|
|
66
|
+
const context = React.useContext(ToggleGroupContext)
|
|
67
|
+
const pressed = Array.isArray(context.value)
|
|
68
|
+
? context.value.includes(value)
|
|
69
|
+
: context.value === value
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Toggle
|
|
73
|
+
ref={ref}
|
|
74
|
+
variant={context.variant}
|
|
75
|
+
size={context.size}
|
|
76
|
+
pressed={pressed}
|
|
77
|
+
onPressedChange={() => context.onValueChange(value)}
|
|
78
|
+
className={cn(className)}
|
|
79
|
+
{...props}
|
|
80
|
+
>
|
|
81
|
+
{children}
|
|
82
|
+
</Toggle>
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
ToggleGroupItem.displayName = "ToggleGroupItem"
|
|
86
|
+
|
|
87
|
+
export { ToggleGroup, ToggleGroupItem }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Pressable, Text, View } from "react-native"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
import { cn } from "../../lib/utils"
|
|
5
|
+
|
|
6
|
+
const toggleVariants = cva(
|
|
7
|
+
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-transparent",
|
|
12
|
+
outline:
|
|
13
|
+
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
|
14
|
+
},
|
|
15
|
+
size: {
|
|
16
|
+
default: "h-10 px-3",
|
|
17
|
+
sm: "h-9 px-2.5",
|
|
18
|
+
lg: "h-11 px-5",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultVariants: {
|
|
22
|
+
variant: "default",
|
|
23
|
+
size: "default",
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const Toggle = React.forwardRef<
|
|
29
|
+
React.ElementRef<typeof Pressable>,
|
|
30
|
+
React.ComponentPropsWithoutRef<typeof Pressable> &
|
|
31
|
+
VariantProps<typeof toggleVariants> & {
|
|
32
|
+
pressed?: boolean
|
|
33
|
+
onPressedChange?: (pressed: boolean) => void
|
|
34
|
+
}
|
|
35
|
+
>(({ className, variant, size, pressed, onPressedChange, children, ...props }, ref) => (
|
|
36
|
+
<Pressable
|
|
37
|
+
ref={ref}
|
|
38
|
+
onPress={() => onPressedChange?.(!pressed)}
|
|
39
|
+
className={cn(
|
|
40
|
+
toggleVariants({ variant, size, className }),
|
|
41
|
+
pressed && "bg-accent text-accent-foreground"
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
|
+
{typeof children === 'string' ? (
|
|
46
|
+
<Text className={cn("text-sm font-medium", pressed ? "text-accent-foreground" : "text-foreground")}>
|
|
47
|
+
{children}
|
|
48
|
+
</Text>
|
|
49
|
+
) : (
|
|
50
|
+
// Inject color prop if child is an icon? For now just render child.
|
|
51
|
+
// Users should style their icons based on pressed state if needed, or we can use a context.
|
|
52
|
+
children
|
|
53
|
+
)}
|
|
54
|
+
</Pressable>
|
|
55
|
+
))
|
|
56
|
+
Toggle.displayName = "Toggle"
|
|
57
|
+
|
|
58
|
+
export { Toggle, toggleVariants }
|
package/src/index.ts
CHANGED
|
@@ -15,4 +15,11 @@ export * from "./components/ui/radio-group"
|
|
|
15
15
|
export * from "./components/ui/textarea"
|
|
16
16
|
export * from "./components/ui/alert"
|
|
17
17
|
export * from "./components/ui/progress"
|
|
18
|
+
export * from "./components/ui/accordion"
|
|
19
|
+
export * from "./components/ui/aspect-ratio"
|
|
20
|
+
export * from "./components/ui/dialog"
|
|
21
|
+
export * from "./components/ui/select"
|
|
22
|
+
export * from "./components/ui/tabs"
|
|
23
|
+
export * from "./components/ui/toggle"
|
|
24
|
+
export * from "./components/ui/toggle-group"
|
|
18
25
|
import "./global.css"
|