@lunar-kit/core 0.1.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/dist/index.d.ts +5 -0
- package/dist/index.js +14 -0
- package/package.json +31 -0
- package/src/components/ui/accordion.tsx +334 -0
- package/src/components/ui/avatar.tsx +326 -0
- package/src/components/ui/badge.tsx +84 -0
- package/src/components/ui/banner.tsx +151 -0
- package/src/components/ui/bottom-sheet.tsx +579 -0
- package/src/components/ui/button.tsx +142 -0
- package/src/components/ui/calendar.tsx +502 -0
- package/src/components/ui/card.tsx +163 -0
- package/src/components/ui/checkbox.tsx +129 -0
- package/src/components/ui/date-picker.tsx +190 -0
- package/src/components/ui/date-range-picker.tsx +262 -0
- package/src/components/ui/dialog.tsx +204 -0
- package/src/components/ui/form.tsx +139 -0
- package/src/components/ui/input.tsx +107 -0
- package/src/components/ui/radio-group.tsx +123 -0
- package/src/components/ui/radio.tsx +109 -0
- package/src/components/ui/select-sheet.tsx +814 -0
- package/src/components/ui/select.tsx +547 -0
- package/src/components/ui/tabs.tsx +254 -0
- package/src/components/ui/text.tsx +229 -0
- package/src/components/ui/textarea.tsx +77 -0
- package/src/components/v0/accordion.tsx +199 -0
- package/src/components/v1/accordion.tsx +234 -0
- package/src/components/v1/avatar.tsx +259 -0
- package/src/components/v1/bottom-sheet.tsx +1090 -0
- package/src/components/v1/button.tsx +61 -0
- package/src/components/v1/calendar.tsx +498 -0
- package/src/components/v1/card.tsx +86 -0
- package/src/components/v1/checkbox.tsx +46 -0
- package/src/components/v1/date-picker.tsx +135 -0
- package/src/components/v1/date-range-picker.tsx +218 -0
- package/src/components/v1/dialog.tsx +211 -0
- package/src/components/v1/radio-group.tsx +76 -0
- package/src/components/v1/select.tsx +217 -0
- package/src/components/v1/tabs.tsx +253 -0
- package/src/registry/ui/accordion.json +30 -0
- package/src/registry/ui/avatar.json +41 -0
- package/src/registry/ui/badge.json +26 -0
- package/src/registry/ui/banner.json +27 -0
- package/src/registry/ui/bottom-sheet.json +29 -0
- package/src/registry/ui/button.json +24 -0
- package/src/registry/ui/calendar.json +29 -0
- package/src/registry/ui/card.json +25 -0
- package/src/registry/ui/checkbox.json +25 -0
- package/src/registry/ui/date-picker.json +30 -0
- package/src/registry/ui/date-range-picker.json +33 -0
- package/src/registry/ui/dialog.json +25 -0
- package/src/registry/ui/form.json +27 -0
- package/src/registry/ui/input.json +22 -0
- package/src/registry/ui/radio-group.json +26 -0
- package/src/registry/ui/radio.json +23 -0
- package/src/registry/ui/select-sheet.json +29 -0
- package/src/registry/ui/select.json +26 -0
- package/src/registry/ui/tabs.json +29 -0
- package/src/registry/ui/text.json +22 -0
- package/src/registry/ui/textarea.json +24 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// components/ui/tabs.tsx
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { View, Pressable, Animated, LayoutChangeEvent } from 'react-native';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
import { Text } from './text';
|
|
6
|
+
|
|
7
|
+
// Context untuk share state
|
|
8
|
+
interface TabsContextValue {
|
|
9
|
+
value: string;
|
|
10
|
+
onValueChange: (value: string) => void;
|
|
11
|
+
variant?: 'pill' | 'underline';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const TabsContext = React.createContext<TabsContextValue | null>(null);
|
|
15
|
+
|
|
16
|
+
const useTabsContext = () => {
|
|
17
|
+
const context = React.useContext(TabsContext);
|
|
18
|
+
if (!context) {
|
|
19
|
+
throw new Error('Tabs components must be used within Tabs');
|
|
20
|
+
}
|
|
21
|
+
return context;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Props Types
|
|
25
|
+
export interface TabsProps {
|
|
26
|
+
value: string;
|
|
27
|
+
onValueChange: (value: string) => void;
|
|
28
|
+
children: React.ReactNode;
|
|
29
|
+
variant?: 'pill' | 'underline';
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TabsListProps {
|
|
34
|
+
children: React.ReactNode;
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TabsTriggerProps {
|
|
39
|
+
value: string;
|
|
40
|
+
children: string;
|
|
41
|
+
className?: string;
|
|
42
|
+
disabled?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface TabsContentProps {
|
|
46
|
+
value: string;
|
|
47
|
+
children: React.ReactNode;
|
|
48
|
+
className?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface TabLayout {
|
|
52
|
+
x: number;
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Extended Context for pill animation
|
|
58
|
+
interface TabsListContextValue extends TabsContextValue {
|
|
59
|
+
registerTab: (value: string, layout: TabLayout) => void;
|
|
60
|
+
tabLayouts: Map<string, TabLayout>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const TabsListContext = React.createContext<TabsListContextValue | null>(null);
|
|
64
|
+
|
|
65
|
+
const useTabsListContext = () => {
|
|
66
|
+
const context = React.useContext(TabsListContext);
|
|
67
|
+
if (!context) {
|
|
68
|
+
throw new Error('TabsTrigger must be used within TabsList');
|
|
69
|
+
}
|
|
70
|
+
return context;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Tabs Root Component
|
|
74
|
+
export function Tabs({
|
|
75
|
+
value,
|
|
76
|
+
onValueChange,
|
|
77
|
+
children,
|
|
78
|
+
variant = 'underline',
|
|
79
|
+
className,
|
|
80
|
+
}: TabsProps) {
|
|
81
|
+
const contextValue: TabsContextValue = {
|
|
82
|
+
value,
|
|
83
|
+
onValueChange,
|
|
84
|
+
variant,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<TabsContext.Provider value={contextValue}>
|
|
89
|
+
<View className={cn('flex', className)}>{children}</View>
|
|
90
|
+
</TabsContext.Provider>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// TabsList Component
|
|
95
|
+
export function TabsList({ children, className }: TabsListProps) {
|
|
96
|
+
const { value, onValueChange, variant } = useTabsContext();
|
|
97
|
+
const [tabLayouts, setTabLayouts] = React.useState<Map<string, TabLayout>>(new Map());
|
|
98
|
+
|
|
99
|
+
const indicatorPosition = React.useRef(new Animated.Value(0)).current;
|
|
100
|
+
const indicatorWidth = React.useRef(new Animated.Value(0)).current;
|
|
101
|
+
const indicatorHeight = React.useRef(new Animated.Value(0)).current;
|
|
102
|
+
|
|
103
|
+
const registerTab = React.useCallback((tabValue: string, layout: TabLayout) => {
|
|
104
|
+
setTabLayouts((prev) => {
|
|
105
|
+
const newMap = new Map(prev);
|
|
106
|
+
newMap.set(tabValue, layout);
|
|
107
|
+
return newMap;
|
|
108
|
+
});
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
// Animate pill indicator when active tab changes
|
|
112
|
+
React.useEffect(() => {
|
|
113
|
+
const activeLayout = tabLayouts.get(value);
|
|
114
|
+
if (activeLayout && variant === 'pill') {
|
|
115
|
+
Animated.parallel([
|
|
116
|
+
Animated.spring(indicatorPosition, {
|
|
117
|
+
toValue: activeLayout.x,
|
|
118
|
+
useNativeDriver: false,
|
|
119
|
+
tension: 100,
|
|
120
|
+
friction: 10,
|
|
121
|
+
}),
|
|
122
|
+
Animated.spring(indicatorWidth, {
|
|
123
|
+
toValue: activeLayout.width,
|
|
124
|
+
useNativeDriver: false,
|
|
125
|
+
tension: 100,
|
|
126
|
+
friction: 10,
|
|
127
|
+
}),
|
|
128
|
+
Animated.spring(indicatorHeight, {
|
|
129
|
+
toValue: activeLayout.height,
|
|
130
|
+
useNativeDriver: false,
|
|
131
|
+
tension: 100,
|
|
132
|
+
friction: 10,
|
|
133
|
+
}),
|
|
134
|
+
]).start();
|
|
135
|
+
}
|
|
136
|
+
}, [value, tabLayouts, variant]);
|
|
137
|
+
|
|
138
|
+
const contextValue: TabsListContextValue = {
|
|
139
|
+
value,
|
|
140
|
+
onValueChange,
|
|
141
|
+
variant,
|
|
142
|
+
registerTab,
|
|
143
|
+
tabLayouts,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<TabsListContext.Provider value={contextValue}>
|
|
148
|
+
<View
|
|
149
|
+
className={cn(
|
|
150
|
+
'relative flex-row items-center',
|
|
151
|
+
variant === 'pill' && 'bg-muted rounded-lg p-1',
|
|
152
|
+
variant === 'underline' && 'border-b border-border',
|
|
153
|
+
className
|
|
154
|
+
)}
|
|
155
|
+
>
|
|
156
|
+
{/* Animated Pill Background */}
|
|
157
|
+
{variant === 'pill' && (
|
|
158
|
+
<Animated.View
|
|
159
|
+
className="absolute bg-background rounded-md shadow-sm"
|
|
160
|
+
style={{
|
|
161
|
+
left: indicatorPosition,
|
|
162
|
+
width: indicatorWidth,
|
|
163
|
+
height: indicatorHeight,
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{children}
|
|
169
|
+
</View>
|
|
170
|
+
</TabsListContext.Provider>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// TabsTrigger Component
|
|
175
|
+
export function TabsTrigger({ value: triggerValue, children, className, disabled = false }: TabsTriggerProps) {
|
|
176
|
+
const { value, onValueChange, variant, registerTab } = useTabsListContext();
|
|
177
|
+
const isActive = value === triggerValue;
|
|
178
|
+
|
|
179
|
+
// Animation for underline
|
|
180
|
+
const scaleAnim = React.useRef(new Animated.Value(isActive ? 1 : 0)).current;
|
|
181
|
+
const opacityAnim = React.useRef(new Animated.Value(isActive ? 1 : 0)).current;
|
|
182
|
+
|
|
183
|
+
const handleLayout = (event: LayoutChangeEvent) => {
|
|
184
|
+
const { x, width, height } = event.nativeEvent.layout;
|
|
185
|
+
registerTab(triggerValue, { x, width, height });
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
React.useEffect(() => {
|
|
189
|
+
if (variant === 'underline') {
|
|
190
|
+
Animated.parallel([
|
|
191
|
+
Animated.spring(scaleAnim, {
|
|
192
|
+
toValue: isActive ? 1 : 0,
|
|
193
|
+
useNativeDriver: true,
|
|
194
|
+
tension: 100,
|
|
195
|
+
friction: 10,
|
|
196
|
+
}),
|
|
197
|
+
Animated.timing(opacityAnim, {
|
|
198
|
+
toValue: isActive ? 1 : 0,
|
|
199
|
+
duration: 200,
|
|
200
|
+
useNativeDriver: true,
|
|
201
|
+
}),
|
|
202
|
+
]).start();
|
|
203
|
+
}
|
|
204
|
+
}, [isActive, variant, scaleAnim, opacityAnim]);
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<Pressable
|
|
208
|
+
onPress={() => onValueChange(triggerValue)}
|
|
209
|
+
onLayout={handleLayout}
|
|
210
|
+
disabled={disabled}
|
|
211
|
+
className={cn(
|
|
212
|
+
'flex-1 items-center justify-center',
|
|
213
|
+
variant === 'pill' && 'px-4 py-2 rounded-md z-10',
|
|
214
|
+
disabled && 'opacity-50',
|
|
215
|
+
className
|
|
216
|
+
)}
|
|
217
|
+
>
|
|
218
|
+
<View className="items-center w-full">
|
|
219
|
+
<Text
|
|
220
|
+
size={variant === 'pill' ? 'sm' : 'md'}
|
|
221
|
+
variant="label"
|
|
222
|
+
className={cn(
|
|
223
|
+
isActive ? 'text-foreground' : 'text-muted-foreground'
|
|
224
|
+
)}
|
|
225
|
+
>
|
|
226
|
+
{children}
|
|
227
|
+
</Text>
|
|
228
|
+
|
|
229
|
+
{/* Animated Underline Indicator */}
|
|
230
|
+
{variant === 'underline' && (
|
|
231
|
+
<Animated.View
|
|
232
|
+
className="h-0.5 mt-3 rounded-full bg-primary border-b-2 border-primary"
|
|
233
|
+
style={{
|
|
234
|
+
width: '100%',
|
|
235
|
+
transform: [{ scaleX: scaleAnim }],
|
|
236
|
+
opacity: opacityAnim,
|
|
237
|
+
}}
|
|
238
|
+
/>
|
|
239
|
+
)}
|
|
240
|
+
</View>
|
|
241
|
+
</Pressable>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// TabsContent Component
|
|
246
|
+
export function TabsContent({ value: contentValue, children, className }: TabsContentProps) {
|
|
247
|
+
const { value } = useTabsContext();
|
|
248
|
+
|
|
249
|
+
if (value !== contentValue) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return <View className={cn('pt-4', className)}>{children}</View>;
|
|
254
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// components/ui/text.tsx
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Text as RNText, TextProps as RNTextProps } from 'react-native';
|
|
4
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
|
|
7
|
+
const textVariants = cva(
|
|
8
|
+
'text-foreground', // base style
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: '',
|
|
13
|
+
header: 'font-bold',
|
|
14
|
+
title: 'font-semibold',
|
|
15
|
+
label: 'font-medium',
|
|
16
|
+
body: 'font-normal',
|
|
17
|
+
caption: 'font-normal text-muted-foreground',
|
|
18
|
+
muted: 'text-muted-foreground',
|
|
19
|
+
error: 'text-destructive',
|
|
20
|
+
success: 'text-green-600 dark:text-green-400',
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
sm: '', // akan di-override di compound variants
|
|
24
|
+
md: '', // akan di-override di compound variants
|
|
25
|
+
lg: '', // akan di-override di compound variants
|
|
26
|
+
xl: '', // akan di-override di compound variants
|
|
27
|
+
},
|
|
28
|
+
align: {
|
|
29
|
+
left: 'text-left',
|
|
30
|
+
center: 'text-center',
|
|
31
|
+
right: 'text-right',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
defaultVariants: {
|
|
35
|
+
variant: 'default',
|
|
36
|
+
size: 'md',
|
|
37
|
+
align: 'left',
|
|
38
|
+
},
|
|
39
|
+
// Compound variants - setiap variant punya size mapping sendiri
|
|
40
|
+
compoundVariants: [
|
|
41
|
+
// HEADER sizes
|
|
42
|
+
{
|
|
43
|
+
variant: 'header',
|
|
44
|
+
size: 'sm',
|
|
45
|
+
class: 'text-2xl', // 24px
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
variant: 'header',
|
|
49
|
+
size: 'md',
|
|
50
|
+
class: 'text-3xl', // 30px
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
variant: 'header',
|
|
54
|
+
size: 'lg',
|
|
55
|
+
class: 'text-4xl', // 36px
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
variant: 'header',
|
|
59
|
+
size: 'xl',
|
|
60
|
+
class: 'text-5xl', // 48px
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// TITLE sizes
|
|
64
|
+
{
|
|
65
|
+
variant: 'title',
|
|
66
|
+
size: 'sm',
|
|
67
|
+
class: 'text-lg', // 18px
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
variant: 'title',
|
|
71
|
+
size: 'md',
|
|
72
|
+
class: 'text-xl', // 20px
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
variant: 'title',
|
|
76
|
+
size: 'lg',
|
|
77
|
+
class: 'text-2xl', // 24px
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
variant: 'title',
|
|
81
|
+
size: 'xl',
|
|
82
|
+
class: 'text-3xl', // 30px
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// LABEL sizes
|
|
86
|
+
{
|
|
87
|
+
variant: 'label',
|
|
88
|
+
size: 'sm',
|
|
89
|
+
class: 'text-xs uppercase tracking-wider', // 12px
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
variant: 'label',
|
|
93
|
+
size: 'md',
|
|
94
|
+
class: 'text-sm uppercase tracking-wide', // 14px
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
variant: 'label',
|
|
98
|
+
size: 'lg',
|
|
99
|
+
class: 'text-base uppercase tracking-wide', // 16px
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// BODY sizes
|
|
103
|
+
{
|
|
104
|
+
variant: 'body',
|
|
105
|
+
size: 'sm',
|
|
106
|
+
class: 'text-sm', // 14px
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
variant: 'body',
|
|
110
|
+
size: 'md',
|
|
111
|
+
class: 'text-base', // 16px
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
variant: 'body',
|
|
115
|
+
size: 'lg',
|
|
116
|
+
class: 'text-lg', // 18px
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// CAPTION sizes
|
|
120
|
+
{
|
|
121
|
+
variant: 'caption',
|
|
122
|
+
size: 'sm',
|
|
123
|
+
class: 'text-xs', // 12px
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
variant: 'caption',
|
|
127
|
+
size: 'md',
|
|
128
|
+
class: 'text-sm', // 14px
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
variant: 'caption',
|
|
132
|
+
size: 'lg',
|
|
133
|
+
class: 'text-base', // 16px
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
// MUTED sizes (sama dengan body)
|
|
137
|
+
{
|
|
138
|
+
variant: 'muted',
|
|
139
|
+
size: 'sm',
|
|
140
|
+
class: 'text-sm',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
variant: 'muted',
|
|
144
|
+
size: 'md',
|
|
145
|
+
class: 'text-base',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
variant: 'muted',
|
|
149
|
+
size: 'lg',
|
|
150
|
+
class: 'text-lg',
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// ERROR sizes (sama dengan body)
|
|
154
|
+
{
|
|
155
|
+
variant: 'error',
|
|
156
|
+
size: 'sm',
|
|
157
|
+
class: 'text-sm',
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
variant: 'error',
|
|
161
|
+
size: 'md',
|
|
162
|
+
class: 'text-base',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
variant: 'error',
|
|
166
|
+
size: 'lg',
|
|
167
|
+
class: 'text-lg',
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
// SUCCESS sizes (sama dengan body)
|
|
171
|
+
{
|
|
172
|
+
variant: 'success',
|
|
173
|
+
size: 'sm',
|
|
174
|
+
class: 'text-sm',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
variant: 'success',
|
|
178
|
+
size: 'md',
|
|
179
|
+
class: 'text-base',
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
variant: 'success',
|
|
183
|
+
size: 'lg',
|
|
184
|
+
class: 'text-lg',
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
// DEFAULT sizes
|
|
188
|
+
{
|
|
189
|
+
variant: 'default',
|
|
190
|
+
size: 'sm',
|
|
191
|
+
class: 'text-sm',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
variant: 'default',
|
|
195
|
+
size: 'md',
|
|
196
|
+
class: 'text-base',
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
variant: 'default',
|
|
200
|
+
size: 'lg',
|
|
201
|
+
class: 'text-lg',
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
export interface TextProps
|
|
208
|
+
extends RNTextProps,
|
|
209
|
+
VariantProps<typeof textVariants> {
|
|
210
|
+
className?: string;
|
|
211
|
+
variant?: 'default' | 'header' | 'title' | 'label' | 'body' | 'caption' | 'muted' | 'error' | 'success';
|
|
212
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
213
|
+
align?: 'left' | 'center' | 'right';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function Text({
|
|
217
|
+
variant,
|
|
218
|
+
size,
|
|
219
|
+
align,
|
|
220
|
+
className,
|
|
221
|
+
...props
|
|
222
|
+
}: TextProps) {
|
|
223
|
+
return (
|
|
224
|
+
<RNText
|
|
225
|
+
className={cn(textVariants({ variant, size, align }), className)}
|
|
226
|
+
{...props}
|
|
227
|
+
/>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// components/ui/textarea.tsx
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { TextInput, View } from 'react-native';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
import { Text } from './text';
|
|
6
|
+
import { useThemeColors } from '@/hooks/useThemeColors';
|
|
7
|
+
|
|
8
|
+
export interface TextareaProps extends React.ComponentPropsWithoutRef<typeof TextInput> {
|
|
9
|
+
label?: string;
|
|
10
|
+
error?: string;
|
|
11
|
+
containerClassName?: string;
|
|
12
|
+
rows?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const Textarea = React.forwardRef<TextInput, TextareaProps>(
|
|
16
|
+
(
|
|
17
|
+
{
|
|
18
|
+
className,
|
|
19
|
+
containerClassName,
|
|
20
|
+
label,
|
|
21
|
+
error,
|
|
22
|
+
editable = true,
|
|
23
|
+
rows = 4,
|
|
24
|
+
...props
|
|
25
|
+
},
|
|
26
|
+
ref
|
|
27
|
+
) => {
|
|
28
|
+
const { colors } = useThemeColors();
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<View className={cn('w-full', containerClassName)}>
|
|
32
|
+
{label && (
|
|
33
|
+
<Text
|
|
34
|
+
size="sm"
|
|
35
|
+
variant="label"
|
|
36
|
+
className={cn(
|
|
37
|
+
'mb-2',
|
|
38
|
+
error && 'text-destructive'
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
{label}
|
|
42
|
+
</Text>
|
|
43
|
+
)}
|
|
44
|
+
|
|
45
|
+
<TextInput
|
|
46
|
+
ref={ref}
|
|
47
|
+
className={cn(
|
|
48
|
+
'border rounded-lg bg-background px-3 py-3 text-base text-foreground',
|
|
49
|
+
error ? 'border-destructive' : 'border-input',
|
|
50
|
+
'focus:border-ring',
|
|
51
|
+
!editable && 'bg-muted opacity-60',
|
|
52
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
placeholderTextColor={colors.mutedForeground}
|
|
55
|
+
multiline
|
|
56
|
+
numberOfLines={rows}
|
|
57
|
+
textAlignVertical="top"
|
|
58
|
+
editable={editable}
|
|
59
|
+
style={[
|
|
60
|
+
{
|
|
61
|
+
minHeight: rows * 24,
|
|
62
|
+
color: colors.foreground,
|
|
63
|
+
}
|
|
64
|
+
]}
|
|
65
|
+
{...props}
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
{error && (
|
|
69
|
+
<Text size="sm" className="text-destructive mt-2">
|
|
70
|
+
{error}
|
|
71
|
+
</Text>
|
|
72
|
+
)}
|
|
73
|
+
</View>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
Textarea.displayName = 'Textarea';
|