@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.
Files changed (59) hide show
  1. package/dist/index.d.ts +5 -0
  2. package/dist/index.js +14 -0
  3. package/package.json +31 -0
  4. package/src/components/ui/accordion.tsx +334 -0
  5. package/src/components/ui/avatar.tsx +326 -0
  6. package/src/components/ui/badge.tsx +84 -0
  7. package/src/components/ui/banner.tsx +151 -0
  8. package/src/components/ui/bottom-sheet.tsx +579 -0
  9. package/src/components/ui/button.tsx +142 -0
  10. package/src/components/ui/calendar.tsx +502 -0
  11. package/src/components/ui/card.tsx +163 -0
  12. package/src/components/ui/checkbox.tsx +129 -0
  13. package/src/components/ui/date-picker.tsx +190 -0
  14. package/src/components/ui/date-range-picker.tsx +262 -0
  15. package/src/components/ui/dialog.tsx +204 -0
  16. package/src/components/ui/form.tsx +139 -0
  17. package/src/components/ui/input.tsx +107 -0
  18. package/src/components/ui/radio-group.tsx +123 -0
  19. package/src/components/ui/radio.tsx +109 -0
  20. package/src/components/ui/select-sheet.tsx +814 -0
  21. package/src/components/ui/select.tsx +547 -0
  22. package/src/components/ui/tabs.tsx +254 -0
  23. package/src/components/ui/text.tsx +229 -0
  24. package/src/components/ui/textarea.tsx +77 -0
  25. package/src/components/v0/accordion.tsx +199 -0
  26. package/src/components/v1/accordion.tsx +234 -0
  27. package/src/components/v1/avatar.tsx +259 -0
  28. package/src/components/v1/bottom-sheet.tsx +1090 -0
  29. package/src/components/v1/button.tsx +61 -0
  30. package/src/components/v1/calendar.tsx +498 -0
  31. package/src/components/v1/card.tsx +86 -0
  32. package/src/components/v1/checkbox.tsx +46 -0
  33. package/src/components/v1/date-picker.tsx +135 -0
  34. package/src/components/v1/date-range-picker.tsx +218 -0
  35. package/src/components/v1/dialog.tsx +211 -0
  36. package/src/components/v1/radio-group.tsx +76 -0
  37. package/src/components/v1/select.tsx +217 -0
  38. package/src/components/v1/tabs.tsx +253 -0
  39. package/src/registry/ui/accordion.json +30 -0
  40. package/src/registry/ui/avatar.json +41 -0
  41. package/src/registry/ui/badge.json +26 -0
  42. package/src/registry/ui/banner.json +27 -0
  43. package/src/registry/ui/bottom-sheet.json +29 -0
  44. package/src/registry/ui/button.json +24 -0
  45. package/src/registry/ui/calendar.json +29 -0
  46. package/src/registry/ui/card.json +25 -0
  47. package/src/registry/ui/checkbox.json +25 -0
  48. package/src/registry/ui/date-picker.json +30 -0
  49. package/src/registry/ui/date-range-picker.json +33 -0
  50. package/src/registry/ui/dialog.json +25 -0
  51. package/src/registry/ui/form.json +27 -0
  52. package/src/registry/ui/input.json +22 -0
  53. package/src/registry/ui/radio-group.json +26 -0
  54. package/src/registry/ui/radio.json +23 -0
  55. package/src/registry/ui/select-sheet.json +29 -0
  56. package/src/registry/ui/select.json +26 -0
  57. package/src/registry/ui/tabs.json +29 -0
  58. package/src/registry/ui/text.json +22 -0
  59. 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
+ }