@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,135 @@
|
|
|
1
|
+
// components/ui/date-picker.tsx
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { View, Text, Pressable } from 'react-native';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
import { Dialog, DialogContent } from './dialog';
|
|
6
|
+
import { Calendar } from './calendar';
|
|
7
|
+
import dayjs from 'dayjs';
|
|
8
|
+
|
|
9
|
+
type DatePickerVariant = 'date' | 'month' | 'year';
|
|
10
|
+
|
|
11
|
+
interface DatePickerProps {
|
|
12
|
+
value?: Date;
|
|
13
|
+
onValueChange?: (date: Date) => void;
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
variant?: DatePickerVariant;
|
|
16
|
+
minDate?: Date;
|
|
17
|
+
maxDate?: Date;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface DatePickerTriggerProps {
|
|
21
|
+
className?: string;
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface DatePickerContentProps {
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DatePickerContext = React.createContext<{
|
|
30
|
+
value?: Date;
|
|
31
|
+
onValueChange?: (date: Date) => void;
|
|
32
|
+
open: boolean;
|
|
33
|
+
setOpen: (open: boolean) => void;
|
|
34
|
+
variant: DatePickerVariant;
|
|
35
|
+
minDate?: Date;
|
|
36
|
+
maxDate?: Date;
|
|
37
|
+
} | null>(null);
|
|
38
|
+
|
|
39
|
+
function useDatePicker() {
|
|
40
|
+
const context = React.useContext(DatePickerContext);
|
|
41
|
+
if (!context) {
|
|
42
|
+
throw new Error('DatePicker components must be used within DatePicker');
|
|
43
|
+
}
|
|
44
|
+
return context;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function DatePicker({
|
|
48
|
+
value,
|
|
49
|
+
onValueChange,
|
|
50
|
+
children,
|
|
51
|
+
variant = 'date',
|
|
52
|
+
minDate,
|
|
53
|
+
maxDate,
|
|
54
|
+
}: DatePickerProps) {
|
|
55
|
+
const [open, setOpen] = React.useState(false);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<DatePickerContext.Provider
|
|
59
|
+
value={{
|
|
60
|
+
value,
|
|
61
|
+
onValueChange,
|
|
62
|
+
open,
|
|
63
|
+
setOpen,
|
|
64
|
+
variant,
|
|
65
|
+
minDate,
|
|
66
|
+
maxDate,
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
70
|
+
{children}
|
|
71
|
+
</Dialog>
|
|
72
|
+
</DatePickerContext.Provider>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function DatePickerTrigger({ className, children }: DatePickerTriggerProps) {
|
|
77
|
+
const { setOpen } = useDatePicker();
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Pressable
|
|
81
|
+
onPress={() => setOpen(true)}
|
|
82
|
+
className={cn(
|
|
83
|
+
'flex-row items-center justify-between px-4 py-3 border border-slate-300 rounded-lg bg-white',
|
|
84
|
+
className
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
{children}
|
|
88
|
+
</Pressable>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function DatePickerContent({ className }: DatePickerContentProps) {
|
|
93
|
+
const { value, onValueChange, setOpen, variant, minDate, maxDate } = useDatePicker();
|
|
94
|
+
|
|
95
|
+
const handleValueChange = (date: Date) => {
|
|
96
|
+
onValueChange?.(date);
|
|
97
|
+
setOpen(false);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<DialogContent className={cn('p-4', className)}>
|
|
102
|
+
<Calendar
|
|
103
|
+
value={value}
|
|
104
|
+
onValueChange={handleValueChange}
|
|
105
|
+
variant={variant}
|
|
106
|
+
minDate={minDate}
|
|
107
|
+
maxDate={maxDate}
|
|
108
|
+
/>
|
|
109
|
+
</DialogContent>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function DatePickerValue({
|
|
114
|
+
placeholder = 'Select date',
|
|
115
|
+
format = 'MMM DD, YYYY',
|
|
116
|
+
className,
|
|
117
|
+
}: {
|
|
118
|
+
placeholder?: string;
|
|
119
|
+
format?: string;
|
|
120
|
+
className?: string;
|
|
121
|
+
}) {
|
|
122
|
+
const { value } = useDatePicker();
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<Text
|
|
126
|
+
className={cn(
|
|
127
|
+
'flex-1 text-sm',
|
|
128
|
+
value ? 'text-slate-900' : 'text-slate-400',
|
|
129
|
+
className
|
|
130
|
+
)}
|
|
131
|
+
>
|
|
132
|
+
{value ? dayjs(value).format(format) : placeholder}
|
|
133
|
+
</Text>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// components/ui/date-range-picker.tsx
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { View, Text, Pressable } from 'react-native';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
import { Dialog, DialogContent } from './dialog';
|
|
6
|
+
import { Calendar } from './calendar';
|
|
7
|
+
import dayjs from 'dayjs';
|
|
8
|
+
|
|
9
|
+
interface DateRangePickerProps {
|
|
10
|
+
startDate?: Date;
|
|
11
|
+
endDate?: Date;
|
|
12
|
+
onRangeChange?: (startDate: Date | undefined, endDate: Date | undefined) => void;
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
minDate?: Date;
|
|
15
|
+
maxDate?: Date;
|
|
16
|
+
maxDays?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface DateRangePickerTriggerProps {
|
|
20
|
+
className?: string;
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface DateRangePickerContentProps {
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DateRangePickerContext = React.createContext<{
|
|
29
|
+
startDate?: Date;
|
|
30
|
+
endDate?: Date;
|
|
31
|
+
onRangeChange?: (startDate: Date | undefined, endDate: Date | undefined) => void;
|
|
32
|
+
open: boolean;
|
|
33
|
+
setOpen: (open: boolean) => void;
|
|
34
|
+
minDate?: Date;
|
|
35
|
+
maxDate?: Date;
|
|
36
|
+
maxDays?: number;
|
|
37
|
+
} | null>(null);
|
|
38
|
+
|
|
39
|
+
function useDateRangePicker() {
|
|
40
|
+
const context = React.useContext(DateRangePickerContext);
|
|
41
|
+
if (!context) {
|
|
42
|
+
throw new Error('DateRangePicker components must be used within DateRangePicker');
|
|
43
|
+
}
|
|
44
|
+
return context;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function DateRangePicker({
|
|
48
|
+
startDate,
|
|
49
|
+
endDate,
|
|
50
|
+
onRangeChange,
|
|
51
|
+
children,
|
|
52
|
+
minDate,
|
|
53
|
+
maxDate,
|
|
54
|
+
maxDays,
|
|
55
|
+
}: DateRangePickerProps) {
|
|
56
|
+
const [open, setOpen] = React.useState(false);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<DateRangePickerContext.Provider
|
|
60
|
+
value={{
|
|
61
|
+
startDate,
|
|
62
|
+
endDate,
|
|
63
|
+
onRangeChange,
|
|
64
|
+
open,
|
|
65
|
+
setOpen,
|
|
66
|
+
minDate,
|
|
67
|
+
maxDate,
|
|
68
|
+
maxDays,
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
72
|
+
{children}
|
|
73
|
+
</Dialog>
|
|
74
|
+
</DateRangePickerContext.Provider>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function DateRangePickerTrigger({ className, children }: DateRangePickerTriggerProps) {
|
|
79
|
+
const { setOpen } = useDateRangePicker();
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Pressable
|
|
83
|
+
onPress={() => setOpen(true)}
|
|
84
|
+
className={cn(
|
|
85
|
+
'flex-row items-center justify-between px-4 py-3 border border-slate-300 rounded-lg bg-white',
|
|
86
|
+
className
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
{children}
|
|
90
|
+
</Pressable>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function DateRangePickerContent({ className }: DateRangePickerContentProps) {
|
|
95
|
+
const { startDate, endDate, onRangeChange, setOpen, minDate, maxDate, maxDays } =
|
|
96
|
+
useDateRangePicker();
|
|
97
|
+
|
|
98
|
+
const [tempStartDate, setTempStartDate] = React.useState<Date | undefined>(startDate);
|
|
99
|
+
const [tempEndDate, setTempEndDate] = React.useState<Date | undefined>(endDate);
|
|
100
|
+
|
|
101
|
+
const handleRangeChange = (start: Date | undefined, end: Date | undefined) => {
|
|
102
|
+
setTempStartDate(start);
|
|
103
|
+
setTempEndDate(end);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleApply = () => {
|
|
107
|
+
if (tempStartDate && tempEndDate) {
|
|
108
|
+
onRangeChange?.(tempStartDate, tempEndDate);
|
|
109
|
+
setOpen(false);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleClear = () => {
|
|
114
|
+
setTempStartDate(undefined);
|
|
115
|
+
setTempEndDate(undefined);
|
|
116
|
+
onRangeChange?.(undefined, undefined);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handleCancel = () => {
|
|
120
|
+
setTempStartDate(startDate);
|
|
121
|
+
setTempEndDate(endDate);
|
|
122
|
+
setOpen(false);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<DialogContent className={cn('p-4', className)}>
|
|
127
|
+
<View>
|
|
128
|
+
{/* Calendar with range mode */}
|
|
129
|
+
<Calendar
|
|
130
|
+
mode="range"
|
|
131
|
+
startDate={tempStartDate}
|
|
132
|
+
endDate={tempEndDate}
|
|
133
|
+
onRangeChange={handleRangeChange}
|
|
134
|
+
minDate={minDate}
|
|
135
|
+
maxDate={maxDate}
|
|
136
|
+
maxDays={maxDays}
|
|
137
|
+
/>
|
|
138
|
+
|
|
139
|
+
{/* Selected Range Info */}
|
|
140
|
+
{tempStartDate && (
|
|
141
|
+
<View className="mt-4 p-3 bg-blue-50 rounded-lg">
|
|
142
|
+
<Text className="text-sm font-medium text-blue-900">
|
|
143
|
+
{tempEndDate
|
|
144
|
+
? `${dayjs(tempStartDate).format('MMM DD, YYYY')} - ${dayjs(tempEndDate).format('MMM DD, YYYY')}`
|
|
145
|
+
: `Start: ${dayjs(tempStartDate).format('MMM DD, YYYY')} (Select end date)`}
|
|
146
|
+
</Text>
|
|
147
|
+
{tempStartDate && tempEndDate && (
|
|
148
|
+
<Text className="text-xs text-blue-700 mt-1">
|
|
149
|
+
{dayjs(tempEndDate).diff(dayjs(tempStartDate), 'day') + 1} days
|
|
150
|
+
</Text>
|
|
151
|
+
)}
|
|
152
|
+
</View>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
{/* Action Buttons */}
|
|
156
|
+
<View className="flex-row gap-2 mt-4">
|
|
157
|
+
<Pressable
|
|
158
|
+
onPress={handleClear}
|
|
159
|
+
className="flex-1 py-2.5 rounded-lg border border-slate-300 items-center"
|
|
160
|
+
>
|
|
161
|
+
<Text className="text-sm font-medium text-slate-700">Clear</Text>
|
|
162
|
+
</Pressable>
|
|
163
|
+
|
|
164
|
+
<Pressable
|
|
165
|
+
onPress={handleCancel}
|
|
166
|
+
className="flex-1 py-2.5 rounded-lg border border-slate-300 items-center"
|
|
167
|
+
>
|
|
168
|
+
<Text className="text-sm font-medium text-slate-700">Cancel</Text>
|
|
169
|
+
</Pressable>
|
|
170
|
+
|
|
171
|
+
<Pressable
|
|
172
|
+
onPress={handleApply}
|
|
173
|
+
disabled={!tempStartDate || !tempEndDate}
|
|
174
|
+
className={cn(
|
|
175
|
+
'flex-1 py-2.5 rounded-lg items-center',
|
|
176
|
+
tempStartDate && tempEndDate ? 'bg-blue-600' : 'bg-slate-200'
|
|
177
|
+
)}
|
|
178
|
+
>
|
|
179
|
+
<Text
|
|
180
|
+
className={cn(
|
|
181
|
+
'text-sm font-medium',
|
|
182
|
+
tempStartDate && tempEndDate ? 'text-white' : 'text-slate-400'
|
|
183
|
+
)}
|
|
184
|
+
>
|
|
185
|
+
Apply
|
|
186
|
+
</Text>
|
|
187
|
+
</Pressable>
|
|
188
|
+
</View>
|
|
189
|
+
</View>
|
|
190
|
+
</DialogContent>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function DateRangePickerValue({
|
|
195
|
+
placeholder = 'Select date range',
|
|
196
|
+
format = 'MMM DD, YYYY',
|
|
197
|
+
className,
|
|
198
|
+
}: {
|
|
199
|
+
placeholder?: string;
|
|
200
|
+
format?: string;
|
|
201
|
+
className?: string;
|
|
202
|
+
}) {
|
|
203
|
+
const { startDate, endDate } = useDateRangePicker();
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<Text
|
|
207
|
+
className={cn(
|
|
208
|
+
'flex-1 text-sm',
|
|
209
|
+
startDate && endDate ? 'text-slate-900' : 'text-slate-400',
|
|
210
|
+
className
|
|
211
|
+
)}
|
|
212
|
+
>
|
|
213
|
+
{startDate && endDate
|
|
214
|
+
? `${dayjs(startDate).format(format)} - ${dayjs(endDate).format(format)}`
|
|
215
|
+
: placeholder}
|
|
216
|
+
</Text>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Modal, View, Text, Pressable, Animated } from 'react-native';
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
|
|
5
|
+
interface DialogProps {
|
|
6
|
+
open?: boolean;
|
|
7
|
+
onOpenChange?: (open: boolean) => void;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface DialogContentProps {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface DialogHeaderProps {
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface DialogTitleProps {
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface DialogDescriptionProps {
|
|
27
|
+
children: React.ReactNode;
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface DialogFooterProps {
|
|
32
|
+
children: React.ReactNode;
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const DialogContext = React.createContext<{
|
|
37
|
+
open: boolean;
|
|
38
|
+
onOpenChange: (open: boolean) => void;
|
|
39
|
+
} | null>(null);
|
|
40
|
+
|
|
41
|
+
function useDialog() {
|
|
42
|
+
const context = React.useContext(DialogContext);
|
|
43
|
+
if (!context) {
|
|
44
|
+
throw new Error('Dialog components must be used within Dialog');
|
|
45
|
+
}
|
|
46
|
+
return context;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function Dialog({ open: controlledOpen, onOpenChange: controlledOnOpenChange, children }: DialogProps) {
|
|
50
|
+
const [internalOpen, setInternalOpen] = React.useState(false);
|
|
51
|
+
|
|
52
|
+
const hasDialogTrigger = React.Children.toArray(children).some(
|
|
53
|
+
(child) => React.isValidElement(child) && child.type === DialogTrigger
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const open = controlledOpen !== undefined ? controlledOpen : internalOpen;
|
|
57
|
+
const onOpenChange = controlledOnOpenChange || setInternalOpen;
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<DialogContext.Provider value={{ open, onOpenChange }}>
|
|
61
|
+
{children}
|
|
62
|
+
</DialogContext.Provider>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function DialogTrigger({ children }: { children: React.ReactNode }) {
|
|
67
|
+
const { onOpenChange } = useDialog();
|
|
68
|
+
|
|
69
|
+
if (React.isValidElement(children)) {
|
|
70
|
+
return React.cloneElement(children as React.ReactElement<any>, {
|
|
71
|
+
onPress: () => onOpenChange(true),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<Pressable onPress={() => onOpenChange(true)}>
|
|
77
|
+
{children}
|
|
78
|
+
</Pressable>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function DialogContent({ children, className }: DialogContentProps) {
|
|
83
|
+
const { open, onOpenChange } = useDialog();
|
|
84
|
+
|
|
85
|
+
// DONE: Track visible state separately for animation
|
|
86
|
+
const [visible, setVisible] = React.useState(false);
|
|
87
|
+
|
|
88
|
+
const scaleAnim = React.useRef(new Animated.Value(0.9)).current;
|
|
89
|
+
const opacityAnim = React.useRef(new Animated.Value(0)).current;
|
|
90
|
+
|
|
91
|
+
React.useEffect(() => {
|
|
92
|
+
if (open) {
|
|
93
|
+
// Show modal first
|
|
94
|
+
setVisible(true);
|
|
95
|
+
|
|
96
|
+
// Then animate in
|
|
97
|
+
Animated.parallel([
|
|
98
|
+
Animated.spring(scaleAnim, {
|
|
99
|
+
toValue: 1,
|
|
100
|
+
useNativeDriver: true,
|
|
101
|
+
tension: 80,
|
|
102
|
+
friction: 10,
|
|
103
|
+
}),
|
|
104
|
+
Animated.timing(opacityAnim, {
|
|
105
|
+
toValue: 1,
|
|
106
|
+
duration: 200,
|
|
107
|
+
useNativeDriver: true,
|
|
108
|
+
}),
|
|
109
|
+
]).start();
|
|
110
|
+
} else {
|
|
111
|
+
// Animate out first
|
|
112
|
+
Animated.parallel([
|
|
113
|
+
Animated.timing(scaleAnim, {
|
|
114
|
+
toValue: 0.9,
|
|
115
|
+
duration: 150,
|
|
116
|
+
useNativeDriver: true,
|
|
117
|
+
}),
|
|
118
|
+
Animated.timing(opacityAnim, {
|
|
119
|
+
toValue: 0,
|
|
120
|
+
duration: 150,
|
|
121
|
+
useNativeDriver: true,
|
|
122
|
+
}),
|
|
123
|
+
]).start(() => {
|
|
124
|
+
// Then hide modal after animation completes
|
|
125
|
+
setVisible(false);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}, [open]);
|
|
129
|
+
|
|
130
|
+
if (!visible) return null;
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<Modal
|
|
134
|
+
visible={visible}
|
|
135
|
+
transparent
|
|
136
|
+
animationType="none"
|
|
137
|
+
onRequestClose={() => onOpenChange(false)}
|
|
138
|
+
>
|
|
139
|
+
<Pressable
|
|
140
|
+
onPress={() => onOpenChange(false)}
|
|
141
|
+
className="flex-1 bg-black/50 items-center justify-center p-4"
|
|
142
|
+
>
|
|
143
|
+
<Animated.View
|
|
144
|
+
style={{
|
|
145
|
+
transform: [{ scale: scaleAnim }],
|
|
146
|
+
opacity: opacityAnim,
|
|
147
|
+
}}
|
|
148
|
+
className="w-full max-w-md"
|
|
149
|
+
>
|
|
150
|
+
<Pressable
|
|
151
|
+
onPress={(e) => e.stopPropagation()}
|
|
152
|
+
className={cn(
|
|
153
|
+
'bg-white rounded-lg p-6 shadow-lg',
|
|
154
|
+
className
|
|
155
|
+
)}
|
|
156
|
+
>
|
|
157
|
+
{children}
|
|
158
|
+
</Pressable>
|
|
159
|
+
</Animated.View>
|
|
160
|
+
</Pressable>
|
|
161
|
+
</Modal>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function DialogHeader({ children, className }: DialogHeaderProps) {
|
|
166
|
+
return (
|
|
167
|
+
<View className={cn('mb-4', className)}>
|
|
168
|
+
{children}
|
|
169
|
+
</View>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function DialogTitle({ children, className }: DialogTitleProps) {
|
|
174
|
+
return (
|
|
175
|
+
<Text className={cn('text-xl font-semibold text-slate-900', className)}>
|
|
176
|
+
{children}
|
|
177
|
+
</Text>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function DialogDescription({ children, className }: DialogDescriptionProps) {
|
|
182
|
+
return (
|
|
183
|
+
<Text className={cn('text-sm text-slate-500 mt-2', className)}>
|
|
184
|
+
{children}
|
|
185
|
+
</Text>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function DialogFooter({ children, className }: DialogFooterProps) {
|
|
190
|
+
return (
|
|
191
|
+
<View className={cn('flex-row justify-end gap-2 mt-6', className)}>
|
|
192
|
+
{children}
|
|
193
|
+
</View>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function DialogClose({ children }: { children: React.ReactNode }) {
|
|
198
|
+
const { onOpenChange } = useDialog();
|
|
199
|
+
|
|
200
|
+
if (React.isValidElement(children)) {
|
|
201
|
+
return React.cloneElement(children as React.ReactElement<any>, {
|
|
202
|
+
onPress: () => onOpenChange(false),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<Pressable onPress={() => onOpenChange(false)}>
|
|
208
|
+
{children}
|
|
209
|
+
</Pressable>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// components/ui/radio-group.tsx
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { View, Text, Pressable } from 'react-native';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
|
|
6
|
+
interface RadioGroupProps {
|
|
7
|
+
value?: string;
|
|
8
|
+
onValueChange?: (value: string) => void;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface RadioGroupItemProps {
|
|
14
|
+
value: string;
|
|
15
|
+
id?: string;
|
|
16
|
+
children?: React.ReactNode;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const RadioGroupContext = React.createContext<{
|
|
21
|
+
value?: string;
|
|
22
|
+
onValueChange?: (value: string) => void;
|
|
23
|
+
} | null>(null);
|
|
24
|
+
|
|
25
|
+
function useRadioGroup() {
|
|
26
|
+
const context = React.useContext(RadioGroupContext);
|
|
27
|
+
if (!context) {
|
|
28
|
+
throw new Error('RadioGroupItem must be used within RadioGroup');
|
|
29
|
+
}
|
|
30
|
+
return context;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function RadioGroup({ value, onValueChange, children, className }: RadioGroupProps) {
|
|
34
|
+
return (
|
|
35
|
+
<RadioGroupContext.Provider value={{ value, onValueChange }}>
|
|
36
|
+
<View className={cn('gap-3', className)}>
|
|
37
|
+
{children}
|
|
38
|
+
</View>
|
|
39
|
+
</RadioGroupContext.Provider>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function RadioGroupItem({ value, id, children, className }: RadioGroupItemProps) {
|
|
44
|
+
const { value: selectedValue, onValueChange } = useRadioGroup();
|
|
45
|
+
const isSelected = selectedValue === value;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Pressable
|
|
49
|
+
onPress={() => onValueChange?.(value)}
|
|
50
|
+
className={cn('flex-row items-center gap-3', className)}
|
|
51
|
+
>
|
|
52
|
+
{/* Radio Circle */}
|
|
53
|
+
<View
|
|
54
|
+
className={cn(
|
|
55
|
+
'h-5 w-5 rounded-full border-2 items-center justify-center',
|
|
56
|
+
isSelected ? 'border-blue-600' : 'border-slate-300'
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
{isSelected && (
|
|
60
|
+
<View className="h-2.5 w-2.5 rounded-full bg-blue-600" />
|
|
61
|
+
)}
|
|
62
|
+
</View>
|
|
63
|
+
|
|
64
|
+
{/* Label */}
|
|
65
|
+
{children}
|
|
66
|
+
</Pressable>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function RadioGroupLabel({ children, className }: { children: React.ReactNode; className?: string }) {
|
|
71
|
+
return (
|
|
72
|
+
<Text className={cn('text-base text-slate-900', className)}>
|
|
73
|
+
{children}
|
|
74
|
+
</Text>
|
|
75
|
+
);
|
|
76
|
+
}
|