@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,217 @@
|
|
|
1
|
+
// components/ui/select.tsx
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { Pressable, ScrollView, Text, View } from 'react-native';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
import { Dialog, DialogContent } from './dialog';
|
|
6
|
+
|
|
7
|
+
interface SelectOption {
|
|
8
|
+
label: string;
|
|
9
|
+
value: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SelectProps {
|
|
13
|
+
value?: string;
|
|
14
|
+
onValueChange?: (value: string) => void;
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface SelectTriggerProps {
|
|
20
|
+
className?: string;
|
|
21
|
+
children?: React.ReactNode;
|
|
22
|
+
size?: 'sm' | 'md' | 'lg';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SelectValueProps {
|
|
26
|
+
placeholder?: string;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface SelectContentProps {
|
|
31
|
+
className?: string;
|
|
32
|
+
children: React.ReactNode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface SelectItemProps {
|
|
36
|
+
value: string;
|
|
37
|
+
label: string;
|
|
38
|
+
className?: string;
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface SelectGroupProps {
|
|
43
|
+
className?: string;
|
|
44
|
+
children: React.ReactNode;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface SelectLabelProps {
|
|
48
|
+
className?: string;
|
|
49
|
+
children: React.ReactNode;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface SelectSeparatorProps {
|
|
53
|
+
className?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const SelectContext = React.createContext<{
|
|
57
|
+
value?: string;
|
|
58
|
+
onValueChange?: (value: string) => void;
|
|
59
|
+
open: boolean;
|
|
60
|
+
setOpen: (open: boolean) => void;
|
|
61
|
+
disabled?: boolean;
|
|
62
|
+
options: Map<string, string>;
|
|
63
|
+
registerOption: (value: string, label: string) => void;
|
|
64
|
+
} | null>(null);
|
|
65
|
+
|
|
66
|
+
function useSelect() {
|
|
67
|
+
const context = React.useContext(SelectContext);
|
|
68
|
+
if (!context) {
|
|
69
|
+
throw new Error('Select components must be used within Select');
|
|
70
|
+
}
|
|
71
|
+
return context;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function Select({ value, onValueChange, children, disabled = false }: SelectProps) {
|
|
75
|
+
const [open, setOpen] = React.useState(false);
|
|
76
|
+
const [options] = React.useState(() => new Map<string, string>());
|
|
77
|
+
|
|
78
|
+
const registerOption = React.useCallback((val: string, label: string) => {
|
|
79
|
+
options.set(val, label);
|
|
80
|
+
}, [options]);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<SelectContext.Provider
|
|
84
|
+
value={{
|
|
85
|
+
value,
|
|
86
|
+
onValueChange,
|
|
87
|
+
open,
|
|
88
|
+
setOpen: (val) => !disabled && setOpen(val),
|
|
89
|
+
disabled,
|
|
90
|
+
options,
|
|
91
|
+
registerOption,
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
95
|
+
{children}
|
|
96
|
+
</Dialog>
|
|
97
|
+
</SelectContext.Provider>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function SelectTrigger({ className, children, size = 'md' }: SelectTriggerProps) {
|
|
102
|
+
const { setOpen, disabled } = useSelect();
|
|
103
|
+
|
|
104
|
+
const sizeStyles = {
|
|
105
|
+
sm: 'h-9 px-3 py-2',
|
|
106
|
+
md: 'h-10 px-4 py-3',
|
|
107
|
+
lg: 'h-12 px-4 py-3.5',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<Pressable
|
|
112
|
+
onPress={() => setOpen(true)}
|
|
113
|
+
disabled={disabled}
|
|
114
|
+
className={cn(
|
|
115
|
+
'flex-row items-center justify-between gap-2 rounded-lg border border-slate-300 bg-white shadow-sm',
|
|
116
|
+
sizeStyles[size],
|
|
117
|
+
disabled && 'opacity-50',
|
|
118
|
+
className
|
|
119
|
+
)}
|
|
120
|
+
>
|
|
121
|
+
{children}
|
|
122
|
+
<Text className="text-slate-500 text-xs">▼</Text>
|
|
123
|
+
</Pressable>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function SelectValue({ placeholder = 'Select...', className }: SelectValueProps) {
|
|
128
|
+
const { value, options } = useSelect();
|
|
129
|
+
const displayValue = value ? options.get(value) : null;
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<Text
|
|
133
|
+
className={cn(
|
|
134
|
+
'flex-1 text-sm',
|
|
135
|
+
displayValue ? 'text-slate-900' : 'text-slate-400',
|
|
136
|
+
className
|
|
137
|
+
)}
|
|
138
|
+
>
|
|
139
|
+
{displayValue || placeholder}
|
|
140
|
+
</Text>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function SelectContent({ className, children }: SelectContentProps) {
|
|
145
|
+
return (
|
|
146
|
+
<DialogContent className={cn('p-0 max-h-96', className)}>
|
|
147
|
+
<ScrollView
|
|
148
|
+
showsVerticalScrollIndicator={false}
|
|
149
|
+
bounces={false}
|
|
150
|
+
className="max-h-96"
|
|
151
|
+
>
|
|
152
|
+
<View className="py-1">{children}</View>
|
|
153
|
+
</ScrollView>
|
|
154
|
+
</DialogContent>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function SelectItem({ value, label, className, disabled = false }: SelectItemProps) {
|
|
159
|
+
const { value: selectedValue, onValueChange, setOpen, registerOption } = useSelect();
|
|
160
|
+
const isSelected = selectedValue === value;
|
|
161
|
+
|
|
162
|
+
React.useEffect(() => {
|
|
163
|
+
registerOption(value, label);
|
|
164
|
+
}, [value, label, registerOption]);
|
|
165
|
+
|
|
166
|
+
const handleSelect = () => {
|
|
167
|
+
if (disabled) return;
|
|
168
|
+
onValueChange?.(value);
|
|
169
|
+
setOpen(false);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<Pressable
|
|
174
|
+
onPress={handleSelect}
|
|
175
|
+
disabled={disabled}
|
|
176
|
+
className={cn(
|
|
177
|
+
'flex-row items-center justify-between px-4 py-3 border-b border-slate-100 active:bg-blue-50',
|
|
178
|
+
isSelected && 'bg-blue-50',
|
|
179
|
+
disabled && 'opacity-50',
|
|
180
|
+
className
|
|
181
|
+
)}
|
|
182
|
+
>
|
|
183
|
+
<Text
|
|
184
|
+
className={cn(
|
|
185
|
+
'flex-1 text-sm',
|
|
186
|
+
isSelected ? 'text-blue-600 font-semibold' : 'text-slate-900'
|
|
187
|
+
)}
|
|
188
|
+
>
|
|
189
|
+
{label}
|
|
190
|
+
</Text>
|
|
191
|
+
|
|
192
|
+
{isSelected && (
|
|
193
|
+
<Text className="text-blue-600 font-bold text-sm ml-2">✓</Text>
|
|
194
|
+
)}
|
|
195
|
+
</Pressable>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function SelectGroup({ className, children }: SelectGroupProps) {
|
|
200
|
+
return <View className={cn('py-1', className)}>{children}</View>;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function SelectLabel({ className, children }: SelectLabelProps) {
|
|
204
|
+
return (
|
|
205
|
+
<View className={cn('px-4 py-2', className)}>
|
|
206
|
+
<Text className="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
|
207
|
+
{children}
|
|
208
|
+
</Text>
|
|
209
|
+
</View>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function SelectSeparator({ className }: SelectSeparatorProps) {
|
|
214
|
+
return <View className={cn('h-px bg-slate-200 my-1 mx-2', className)} />;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export type { SelectOption };
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// components/ui/tabs.tsx
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { View, Text, Pressable, Animated, LayoutChangeEvent } from 'react-native';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
|
|
6
|
+
// Context untuk share state
|
|
7
|
+
interface TabsContextValue {
|
|
8
|
+
value: string;
|
|
9
|
+
onValueChange: (value: string) => void;
|
|
10
|
+
variant?: 'pill' | 'underline';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const TabsContext = React.createContext<TabsContextValue | null>(null);
|
|
14
|
+
|
|
15
|
+
const useTabsContext = () => {
|
|
16
|
+
const context = React.useContext(TabsContext);
|
|
17
|
+
if (!context) {
|
|
18
|
+
throw new Error('Tabs components must be used within Tabs');
|
|
19
|
+
}
|
|
20
|
+
return context;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Props Types
|
|
24
|
+
export interface TabsProps {
|
|
25
|
+
value: string;
|
|
26
|
+
onValueChange: (value: string) => void;
|
|
27
|
+
children: React.ReactNode;
|
|
28
|
+
variant?: 'pill' | 'underline';
|
|
29
|
+
className?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TabsListProps {
|
|
33
|
+
children: React.ReactNode;
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TabsTriggerProps {
|
|
38
|
+
value: string;
|
|
39
|
+
children: string;
|
|
40
|
+
className?: string;
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TabsContentProps {
|
|
45
|
+
value: string;
|
|
46
|
+
children: React.ReactNode;
|
|
47
|
+
className?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface TabLayout {
|
|
51
|
+
x: number;
|
|
52
|
+
width: number;
|
|
53
|
+
height: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Extended Context for pill animation
|
|
57
|
+
interface TabsListContextValue extends TabsContextValue {
|
|
58
|
+
registerTab: (value: string, layout: TabLayout) => void;
|
|
59
|
+
tabLayouts: Map<string, TabLayout>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const TabsListContext = React.createContext<TabsListContextValue | null>(null);
|
|
63
|
+
|
|
64
|
+
const useTabsListContext = () => {
|
|
65
|
+
const context = React.useContext(TabsListContext);
|
|
66
|
+
if (!context) {
|
|
67
|
+
throw new Error('TabsTrigger must be used within TabsList');
|
|
68
|
+
}
|
|
69
|
+
return context;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Tabs Root Component
|
|
73
|
+
export function Tabs({
|
|
74
|
+
value,
|
|
75
|
+
onValueChange,
|
|
76
|
+
children,
|
|
77
|
+
variant = 'underline',
|
|
78
|
+
className,
|
|
79
|
+
}: TabsProps) {
|
|
80
|
+
const contextValue: TabsContextValue = {
|
|
81
|
+
value,
|
|
82
|
+
onValueChange,
|
|
83
|
+
variant,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<TabsContext.Provider value={contextValue}>
|
|
88
|
+
<View className={cn('flex', className)}>{children}</View>
|
|
89
|
+
</TabsContext.Provider>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// TabsList Component
|
|
94
|
+
export function TabsList({ children, className }: TabsListProps) {
|
|
95
|
+
const { value, onValueChange, variant } = useTabsContext();
|
|
96
|
+
const [tabLayouts, setTabLayouts] = React.useState<Map<string, TabLayout>>(new Map());
|
|
97
|
+
|
|
98
|
+
const indicatorPosition = React.useRef(new Animated.Value(0)).current;
|
|
99
|
+
const indicatorWidth = React.useRef(new Animated.Value(0)).current;
|
|
100
|
+
const indicatorHeight = React.useRef(new Animated.Value(0)).current;
|
|
101
|
+
|
|
102
|
+
const registerTab = React.useCallback((tabValue: string, layout: TabLayout) => {
|
|
103
|
+
setTabLayouts((prev) => {
|
|
104
|
+
const newMap = new Map(prev);
|
|
105
|
+
newMap.set(tabValue, layout);
|
|
106
|
+
return newMap;
|
|
107
|
+
});
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
110
|
+
// Animate pill indicator when active tab changes
|
|
111
|
+
React.useEffect(() => {
|
|
112
|
+
const activeLayout = tabLayouts.get(value);
|
|
113
|
+
if (activeLayout && variant === 'pill') {
|
|
114
|
+
Animated.parallel([
|
|
115
|
+
Animated.spring(indicatorPosition, {
|
|
116
|
+
toValue: activeLayout.x,
|
|
117
|
+
useNativeDriver: false,
|
|
118
|
+
tension: 100,
|
|
119
|
+
friction: 10,
|
|
120
|
+
}),
|
|
121
|
+
Animated.spring(indicatorWidth, {
|
|
122
|
+
toValue: activeLayout.width,
|
|
123
|
+
useNativeDriver: false,
|
|
124
|
+
tension: 100,
|
|
125
|
+
friction: 10,
|
|
126
|
+
}),
|
|
127
|
+
Animated.spring(indicatorHeight, {
|
|
128
|
+
toValue: activeLayout.height,
|
|
129
|
+
useNativeDriver: false,
|
|
130
|
+
tension: 100,
|
|
131
|
+
friction: 10,
|
|
132
|
+
}),
|
|
133
|
+
]).start();
|
|
134
|
+
}
|
|
135
|
+
}, [value, tabLayouts, variant]);
|
|
136
|
+
|
|
137
|
+
const contextValue: TabsListContextValue = {
|
|
138
|
+
value,
|
|
139
|
+
onValueChange,
|
|
140
|
+
variant,
|
|
141
|
+
registerTab,
|
|
142
|
+
tabLayouts,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<TabsListContext.Provider value={contextValue}>
|
|
147
|
+
<View
|
|
148
|
+
className={cn(
|
|
149
|
+
'relative flex-row items-center',
|
|
150
|
+
variant === 'pill' && 'bg-slate-200 rounded-lg p-1',
|
|
151
|
+
variant === 'underline' && 'border-b border-slate-300',
|
|
152
|
+
className
|
|
153
|
+
)}
|
|
154
|
+
>
|
|
155
|
+
{/* Animated Pill Background */}
|
|
156
|
+
{variant === 'pill' && (
|
|
157
|
+
<Animated.View
|
|
158
|
+
className="absolute bg-white rounded-md shadow-sm"
|
|
159
|
+
style={{
|
|
160
|
+
left: indicatorPosition,
|
|
161
|
+
width: indicatorWidth,
|
|
162
|
+
height: indicatorHeight,
|
|
163
|
+
}}
|
|
164
|
+
/>
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
{children}
|
|
168
|
+
</View>
|
|
169
|
+
</TabsListContext.Provider>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// TabsTrigger Component
|
|
174
|
+
export function TabsTrigger({ value: triggerValue, children, className, disabled = false }: TabsTriggerProps) {
|
|
175
|
+
const { value, onValueChange, variant, registerTab } = useTabsListContext();
|
|
176
|
+
const isActive = value === triggerValue;
|
|
177
|
+
|
|
178
|
+
// Animation for underline
|
|
179
|
+
const scaleAnim = React.useRef(new Animated.Value(isActive ? 1 : 0)).current;
|
|
180
|
+
const opacityAnim = React.useRef(new Animated.Value(isActive ? 1 : 0)).current;
|
|
181
|
+
|
|
182
|
+
const handleLayout = (event: LayoutChangeEvent) => {
|
|
183
|
+
const { x, width, height } = event.nativeEvent.layout;
|
|
184
|
+
registerTab(triggerValue, { x, width, height });
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
React.useEffect(() => {
|
|
188
|
+
if (variant === 'underline') {
|
|
189
|
+
Animated.parallel([
|
|
190
|
+
Animated.spring(scaleAnim, {
|
|
191
|
+
toValue: isActive ? 1 : 0,
|
|
192
|
+
useNativeDriver: true,
|
|
193
|
+
tension: 100,
|
|
194
|
+
friction: 10,
|
|
195
|
+
}),
|
|
196
|
+
Animated.timing(opacityAnim, {
|
|
197
|
+
toValue: isActive ? 1 : 0,
|
|
198
|
+
duration: 200,
|
|
199
|
+
useNativeDriver: true,
|
|
200
|
+
}),
|
|
201
|
+
]).start();
|
|
202
|
+
}
|
|
203
|
+
}, [isActive, variant, scaleAnim, opacityAnim]);
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<Pressable
|
|
207
|
+
onPress={() => onValueChange(triggerValue)}
|
|
208
|
+
onLayout={handleLayout}
|
|
209
|
+
disabled={disabled}
|
|
210
|
+
className={cn(
|
|
211
|
+
'flex-1 items-center justify-center',
|
|
212
|
+
variant === 'pill' && 'px-4 rounded-md z-10',
|
|
213
|
+
disabled && 'opacity-50',
|
|
214
|
+
className
|
|
215
|
+
)}
|
|
216
|
+
>
|
|
217
|
+
<View className="items-center w-full">
|
|
218
|
+
<Text
|
|
219
|
+
className={cn(
|
|
220
|
+
'text-base font-medium',
|
|
221
|
+
isActive ? 'text-slate-900' : 'text-slate-400',
|
|
222
|
+
variant === 'pill' && 'text-sm py-1'
|
|
223
|
+
)}
|
|
224
|
+
>
|
|
225
|
+
{children}
|
|
226
|
+
</Text>
|
|
227
|
+
|
|
228
|
+
{/* Animated Underline Indicator */}
|
|
229
|
+
{variant === 'underline' && (
|
|
230
|
+
<Animated.View
|
|
231
|
+
className="h-0.5 mt-3 rounded-full bg-slate-900 border-b-2 border-slate-900"
|
|
232
|
+
style={{
|
|
233
|
+
width: '100%',
|
|
234
|
+
transform: [{ scaleX: scaleAnim }],
|
|
235
|
+
opacity: opacityAnim,
|
|
236
|
+
}}
|
|
237
|
+
/>
|
|
238
|
+
)}
|
|
239
|
+
</View>
|
|
240
|
+
</Pressable>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// TabsContent Component
|
|
245
|
+
export function TabsContent({ value: contentValue, children, className }: TabsContentProps) {
|
|
246
|
+
const { value } = useTabsContext();
|
|
247
|
+
|
|
248
|
+
if (value !== contentValue) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return <View className={cn('pt-4', className)}>{children}</View>;
|
|
253
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "accordion",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"dependencies": [],
|
|
5
|
+
"devDependencies": [],
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"text"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "/src/components/ui/accordion.tsx",
|
|
12
|
+
"type": "component"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"meta": {
|
|
16
|
+
"description": "A vertically stacked set of interactive headings that reveal or hide associated sections of content. Supports single and multiple expansion modes.",
|
|
17
|
+
"features": [
|
|
18
|
+
"Single mode: only one item can be open at a time",
|
|
19
|
+
"Multiple mode: multiple items can be open simultaneously",
|
|
20
|
+
"Collapsible option for single mode",
|
|
21
|
+
"Smooth animations with LayoutAnimation",
|
|
22
|
+
"Animated chevron rotation indicator",
|
|
23
|
+
"Disabled state support",
|
|
24
|
+
"Helper text components for common patterns",
|
|
25
|
+
"Custom content support",
|
|
26
|
+
"Accessible keyboard navigation",
|
|
27
|
+
"Perfect for FAQs, settings panels, and navigation menus"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "avatar",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"dependencies": [],
|
|
5
|
+
"devDependencies": [],
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"text"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "/src/components/ui/avatar.tsx",
|
|
12
|
+
"type": "component"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"tailwind": {
|
|
16
|
+
"config": {
|
|
17
|
+
"theme": {
|
|
18
|
+
"extend": {
|
|
19
|
+
"colors": {
|
|
20
|
+
"slate": {
|
|
21
|
+
"200": "#e2e8f0",
|
|
22
|
+
"300": "#cbd5e1",
|
|
23
|
+
"400": "#94a3b8",
|
|
24
|
+
"600": "#475569",
|
|
25
|
+
"700": "#334155"
|
|
26
|
+
},
|
|
27
|
+
"green": {
|
|
28
|
+
"500": "#22c55e"
|
|
29
|
+
},
|
|
30
|
+
"yellow": {
|
|
31
|
+
"500": "#eab308"
|
|
32
|
+
},
|
|
33
|
+
"red": {
|
|
34
|
+
"500": "#ef4444"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "badge",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"dependencies": [],
|
|
5
|
+
"devDependencies": [],
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"text"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "/src/components/ui/badge.tsx",
|
|
12
|
+
"type": "component"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"meta": {
|
|
16
|
+
"description": "A small badge component used to display status, counts, or labels. Can be used standalone or in conjunction with other UI elements.",
|
|
17
|
+
"features": [
|
|
18
|
+
"Multiple preset colors for different contexts (e.g., success, warning, error)",
|
|
19
|
+
"Customizable sizes (small, medium, large)",
|
|
20
|
+
"Optional icons to enhance visual representation",
|
|
21
|
+
"Supports text truncation for long content",
|
|
22
|
+
"Accessible with proper ARIA attributes",
|
|
23
|
+
"Lightweight and easy to integrate into existing designs"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "banner",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"dependencies": [],
|
|
5
|
+
"devDependencies": [],
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"text"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "/src/components/ui/banner.tsx",
|
|
12
|
+
"type": "component"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"meta": {
|
|
16
|
+
"description": "A banner component used to display important messages or alerts to users. Can be used for notifications, warnings, or informational messages.",
|
|
17
|
+
"features": [
|
|
18
|
+
"Multiple preset styles for different contexts (e.g., info, success, warning, error)",
|
|
19
|
+
"Customizable icons to enhance visual representation",
|
|
20
|
+
"Optional action buttons for user interaction",
|
|
21
|
+
"Dismissible banners with smooth animations",
|
|
22
|
+
"Supports text truncation for long content",
|
|
23
|
+
"Accessible with proper ARIA attributes",
|
|
24
|
+
"Lightweight and easy to integrate into existing designs"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bottom-sheet",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"description": "A draggable bottom sheet component with snap points",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "/src/components/ui/bottom-sheet.tsx",
|
|
8
|
+
"type": "registry:ui"
|
|
9
|
+
}
|
|
10
|
+
],
|
|
11
|
+
"dependencies": [],
|
|
12
|
+
"registryDependencies": [
|
|
13
|
+
"text",
|
|
14
|
+
"radio",
|
|
15
|
+
"checkbox"
|
|
16
|
+
],
|
|
17
|
+
"meta": {
|
|
18
|
+
"description": "A draggable bottom sheet component with snap points, smooth gestures, and list variants.",
|
|
19
|
+
"features": [
|
|
20
|
+
"Multiple snap points support",
|
|
21
|
+
"Smooth drag gestures with PanResponder",
|
|
22
|
+
"Scrollable body with fixed header and footer",
|
|
23
|
+
"List variants (plain, single-select, multi-select)",
|
|
24
|
+
"Auto-height animation with LayoutAnimation",
|
|
25
|
+
"Backdrop with opacity animation"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "button",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"description": "A customizable button component",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "/src/components/ui/button.tsx",
|
|
8
|
+
"type": "component"
|
|
9
|
+
}
|
|
10
|
+
],
|
|
11
|
+
"dependencies": [],
|
|
12
|
+
"registryDependencies": [],
|
|
13
|
+
"meta": {
|
|
14
|
+
"description": "A customizable button component with various styles and states.",
|
|
15
|
+
"features": [
|
|
16
|
+
"Multiple preset styles (primary, secondary, danger, etc.)",
|
|
17
|
+
"Customizable sizes (small, medium, large)",
|
|
18
|
+
"Supports icons on the left or right side",
|
|
19
|
+
"Disabled and loading states",
|
|
20
|
+
"Accessible with proper ARIA attributes",
|
|
21
|
+
"Lightweight and easy to integrate into existing designs"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "calendar",
|
|
3
|
+
"type": "registry:ui",
|
|
4
|
+
"dependencies": [
|
|
5
|
+
"dayjs"
|
|
6
|
+
],
|
|
7
|
+
"devDependencies": [],
|
|
8
|
+
"registryDependencies": [
|
|
9
|
+
"text"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "/src/components/ui/calendar.tsx",
|
|
14
|
+
"type": "component"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"meta": {
|
|
18
|
+
"description": "A reusable calendar component with date, month, and year views. Supports single selection and range selection modes.",
|
|
19
|
+
"features": [
|
|
20
|
+
"Three view modes: date, month, year",
|
|
21
|
+
"Single and range selection modes",
|
|
22
|
+
"Smart navigation between views",
|
|
23
|
+
"Min/max date constraints",
|
|
24
|
+
"Max days constraint for range mode",
|
|
25
|
+
"Native device locale support",
|
|
26
|
+
"Can be used inline or with DatePicker/DateRangePicker"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|