@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,61 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Pressable, Text } from 'react-native';
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
|
|
5
|
+
interface ButtonProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
variant?: 'default' | 'outline' | 'ghost';
|
|
8
|
+
size?: 'default' | 'sm' | 'lg' | 'icon';
|
|
9
|
+
onPress?: () => void;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Button({
|
|
14
|
+
children,
|
|
15
|
+
variant = 'default',
|
|
16
|
+
size = 'default',
|
|
17
|
+
onPress,
|
|
18
|
+
className,
|
|
19
|
+
}: ButtonProps) {
|
|
20
|
+
return (
|
|
21
|
+
<Pressable
|
|
22
|
+
onPress={onPress}
|
|
23
|
+
className={cn(
|
|
24
|
+
'items-center justify-center rounded-md',
|
|
25
|
+
{
|
|
26
|
+
'bg-slate-900': variant === 'default',
|
|
27
|
+
'border border-slate-200': variant === 'outline',
|
|
28
|
+
'bg-transparent': variant === 'ghost',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
'h-10 px-4': size === 'default',
|
|
32
|
+
'h-9 px-3': size === 'sm',
|
|
33
|
+
'h-11 px-8': size === 'lg',
|
|
34
|
+
'h-10 w-10': size === 'icon', // Square button
|
|
35
|
+
},
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
{/* DONE: Only render Text if not icon size or if children is string */}
|
|
40
|
+
{size === 'icon' && typeof children !== 'string' ? (
|
|
41
|
+
children
|
|
42
|
+
) : (
|
|
43
|
+
<Text
|
|
44
|
+
className={cn(
|
|
45
|
+
'font-medium',
|
|
46
|
+
{
|
|
47
|
+
'text-slate-50': variant === 'default',
|
|
48
|
+
'text-slate-900': variant === 'outline' || variant === 'ghost',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
'text-sm': size === 'default' || size === 'sm',
|
|
52
|
+
'text-base': size === 'lg',
|
|
53
|
+
}
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
</Text>
|
|
58
|
+
)}
|
|
59
|
+
</Pressable>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
// components/ui/calendar.tsx
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { View, Text, Pressable, ScrollView } from 'react-native';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
import dayjs, { Dayjs } from 'dayjs';
|
|
6
|
+
import isBetween from 'dayjs/plugin/isBetween';
|
|
7
|
+
|
|
8
|
+
dayjs.extend(isBetween);
|
|
9
|
+
|
|
10
|
+
type CalendarMode = 'date' | 'month' | 'year';
|
|
11
|
+
type CalendarVariant = 'date' | 'month' | 'year';
|
|
12
|
+
|
|
13
|
+
interface CalendarProps {
|
|
14
|
+
value?: Date;
|
|
15
|
+
onValueChange?: (date: Date) => void;
|
|
16
|
+
variant?: CalendarVariant;
|
|
17
|
+
minDate?: Date;
|
|
18
|
+
maxDate?: Date;
|
|
19
|
+
className?: string;
|
|
20
|
+
// DONE: Range mode props
|
|
21
|
+
mode?: 'single' | 'range';
|
|
22
|
+
startDate?: Date;
|
|
23
|
+
endDate?: Date;
|
|
24
|
+
onRangeChange?: (startDate: Date | undefined, endDate: Date | undefined) => void;
|
|
25
|
+
maxDays?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function Calendar({
|
|
29
|
+
value,
|
|
30
|
+
onValueChange,
|
|
31
|
+
variant = 'date',
|
|
32
|
+
minDate,
|
|
33
|
+
maxDate,
|
|
34
|
+
className,
|
|
35
|
+
mode = 'single',
|
|
36
|
+
startDate,
|
|
37
|
+
endDate,
|
|
38
|
+
onRangeChange,
|
|
39
|
+
maxDays,
|
|
40
|
+
}: CalendarProps) {
|
|
41
|
+
const [currentDate, setCurrentDate] = React.useState<Dayjs>(() =>
|
|
42
|
+
value || startDate ? dayjs(value || startDate) : dayjs()
|
|
43
|
+
);
|
|
44
|
+
const [calendarMode, setCalendarMode] = React.useState<CalendarMode>(variant);
|
|
45
|
+
|
|
46
|
+
// DONE: Range selection state
|
|
47
|
+
const [tempStartDate, setTempStartDate] = React.useState<Dayjs | undefined>(
|
|
48
|
+
startDate ? dayjs(startDate) : undefined
|
|
49
|
+
);
|
|
50
|
+
const [tempEndDate, setTempEndDate] = React.useState<Dayjs | undefined>(
|
|
51
|
+
endDate ? dayjs(endDate) : undefined
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
React.useEffect(() => {
|
|
55
|
+
setCalendarMode(variant);
|
|
56
|
+
}, [variant]);
|
|
57
|
+
|
|
58
|
+
const handleDateSelect = (date: Dayjs) => {
|
|
59
|
+
if (mode === 'range') {
|
|
60
|
+
// Range selection logic
|
|
61
|
+
if (!tempStartDate || (tempStartDate && tempEndDate)) {
|
|
62
|
+
setTempStartDate(date);
|
|
63
|
+
setTempEndDate(undefined);
|
|
64
|
+
} else {
|
|
65
|
+
if (date.isBefore(tempStartDate)) {
|
|
66
|
+
setTempStartDate(date);
|
|
67
|
+
setTempEndDate(tempStartDate);
|
|
68
|
+
} else {
|
|
69
|
+
if (maxDays) {
|
|
70
|
+
const daysDiff = date.diff(tempStartDate, 'day');
|
|
71
|
+
if (daysDiff > maxDays) return;
|
|
72
|
+
}
|
|
73
|
+
setTempEndDate(date);
|
|
74
|
+
}
|
|
75
|
+
// Call range change callback
|
|
76
|
+
const newStart = tempStartDate;
|
|
77
|
+
const newEnd = date.isBefore(tempStartDate) ? tempStartDate : date;
|
|
78
|
+
onRangeChange?.(newStart.toDate(), newEnd.toDate());
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
// Single selection logic
|
|
82
|
+
onValueChange?.(date.toDate());
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleMonthSelect = (monthIndex: number) => {
|
|
87
|
+
const newDate = currentDate.month(monthIndex);
|
|
88
|
+
setCurrentDate(newDate);
|
|
89
|
+
|
|
90
|
+
if (variant === 'month') {
|
|
91
|
+
if (mode === 'single') {
|
|
92
|
+
onValueChange?.(newDate.toDate());
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
setCalendarMode('date');
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const handleYearSelect = (year: number) => {
|
|
100
|
+
const newDate = currentDate.year(year);
|
|
101
|
+
setCurrentDate(newDate);
|
|
102
|
+
|
|
103
|
+
if (variant === 'year') {
|
|
104
|
+
if (mode === 'single') {
|
|
105
|
+
onValueChange?.(newDate.toDate());
|
|
106
|
+
}
|
|
107
|
+
} else if (variant === 'month') {
|
|
108
|
+
setCalendarMode('month');
|
|
109
|
+
} else {
|
|
110
|
+
setCalendarMode('month');
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const handlePrevious = () => {
|
|
115
|
+
if (calendarMode === 'date') {
|
|
116
|
+
setCurrentDate(currentDate.subtract(1, 'month'));
|
|
117
|
+
} else if (calendarMode === 'month') {
|
|
118
|
+
setCurrentDate(currentDate.subtract(1, 'year'));
|
|
119
|
+
} else {
|
|
120
|
+
setCurrentDate(currentDate.subtract(12, 'year'));
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleNext = () => {
|
|
125
|
+
if (calendarMode === 'date') {
|
|
126
|
+
setCurrentDate(currentDate.add(1, 'month'));
|
|
127
|
+
} else if (calendarMode === 'month') {
|
|
128
|
+
setCurrentDate(currentDate.add(1, 'year'));
|
|
129
|
+
} else {
|
|
130
|
+
setCurrentDate(currentDate.add(12, 'year'));
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<View className={className}>
|
|
136
|
+
{/* Header */}
|
|
137
|
+
<View className="flex-row items-center justify-between mb-4">
|
|
138
|
+
<Pressable onPress={handlePrevious} className="p-2">
|
|
139
|
+
<Text className="text-lg font-semibold text-slate-700">←</Text>
|
|
140
|
+
</Pressable>
|
|
141
|
+
|
|
142
|
+
<View className="flex-row items-center gap-2">
|
|
143
|
+
{calendarMode === 'date' && (
|
|
144
|
+
<>
|
|
145
|
+
<Pressable onPress={() => setCalendarMode('month')}>
|
|
146
|
+
<Text className="text-base font-semibold text-slate-900">
|
|
147
|
+
{currentDate.format('MMMM')}
|
|
148
|
+
</Text>
|
|
149
|
+
</Pressable>
|
|
150
|
+
<Pressable onPress={() => setCalendarMode('year')}>
|
|
151
|
+
<Text className="text-base font-semibold text-slate-900">
|
|
152
|
+
{currentDate.format('YYYY')}
|
|
153
|
+
</Text>
|
|
154
|
+
</Pressable>
|
|
155
|
+
</>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{calendarMode === 'month' && (
|
|
159
|
+
<Pressable onPress={() => setCalendarMode('year')}>
|
|
160
|
+
<Text className="text-base font-semibold text-slate-900">
|
|
161
|
+
{currentDate.format('YYYY')}
|
|
162
|
+
</Text>
|
|
163
|
+
</Pressable>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
{calendarMode === 'year' && (
|
|
167
|
+
<Text className="text-base font-semibold text-slate-900">
|
|
168
|
+
{currentDate.year() - 6} - {currentDate.year() + 5}
|
|
169
|
+
</Text>
|
|
170
|
+
)}
|
|
171
|
+
</View>
|
|
172
|
+
|
|
173
|
+
<Pressable onPress={handleNext} className="p-2">
|
|
174
|
+
<Text className="text-lg font-semibold text-slate-700">→</Text>
|
|
175
|
+
</Pressable>
|
|
176
|
+
</View>
|
|
177
|
+
|
|
178
|
+
{/* Content */}
|
|
179
|
+
{calendarMode === 'date' && (
|
|
180
|
+
<DateGrid
|
|
181
|
+
currentDate={currentDate}
|
|
182
|
+
selectedDate={mode === 'single' && value ? dayjs(value) : undefined}
|
|
183
|
+
onSelect={handleDateSelect}
|
|
184
|
+
minDate={minDate}
|
|
185
|
+
maxDate={maxDate}
|
|
186
|
+
// DONE: Pass range props
|
|
187
|
+
mode={mode}
|
|
188
|
+
startDate={tempStartDate}
|
|
189
|
+
endDate={tempEndDate}
|
|
190
|
+
maxDays={maxDays}
|
|
191
|
+
/>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{calendarMode === 'month' && (
|
|
195
|
+
<MonthGrid
|
|
196
|
+
currentDate={currentDate}
|
|
197
|
+
selectedDate={value ? dayjs(value) : undefined}
|
|
198
|
+
onSelect={handleMonthSelect}
|
|
199
|
+
minDate={minDate}
|
|
200
|
+
maxDate={maxDate}
|
|
201
|
+
/>
|
|
202
|
+
)}
|
|
203
|
+
|
|
204
|
+
{calendarMode === 'year' && (
|
|
205
|
+
<YearGrid
|
|
206
|
+
currentDate={currentDate}
|
|
207
|
+
selectedDate={value ? dayjs(value) : undefined}
|
|
208
|
+
onSelect={handleYearSelect}
|
|
209
|
+
minDate={minDate}
|
|
210
|
+
maxDate={maxDate}
|
|
211
|
+
/>
|
|
212
|
+
)}
|
|
213
|
+
</View>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Date Grid (updated with range support)
|
|
218
|
+
function DateGrid({
|
|
219
|
+
currentDate,
|
|
220
|
+
selectedDate,
|
|
221
|
+
onSelect,
|
|
222
|
+
minDate,
|
|
223
|
+
maxDate,
|
|
224
|
+
mode = 'single',
|
|
225
|
+
startDate,
|
|
226
|
+
endDate,
|
|
227
|
+
maxDays,
|
|
228
|
+
}: {
|
|
229
|
+
currentDate: Dayjs;
|
|
230
|
+
selectedDate?: Dayjs;
|
|
231
|
+
onSelect: (date: Dayjs) => void;
|
|
232
|
+
minDate?: Date;
|
|
233
|
+
maxDate?: Date;
|
|
234
|
+
mode?: 'single' | 'range';
|
|
235
|
+
startDate?: Dayjs;
|
|
236
|
+
endDate?: Dayjs;
|
|
237
|
+
maxDays?: number;
|
|
238
|
+
}) {
|
|
239
|
+
const generateCalendarDays = () => {
|
|
240
|
+
const year = currentDate.year();
|
|
241
|
+
const month = currentDate.month();
|
|
242
|
+
|
|
243
|
+
const firstDay = new Date(year, month, 1, 12, 0, 0);
|
|
244
|
+
const lastDay = new Date(year, month + 1, 0, 12, 0, 0);
|
|
245
|
+
|
|
246
|
+
const startDayOfWeek = firstDay.getDay();
|
|
247
|
+
const daysInMonth = lastDay.getDate();
|
|
248
|
+
|
|
249
|
+
const startDateCalc = new Date(year, month, 1 - startDayOfWeek, 12, 0, 0);
|
|
250
|
+
|
|
251
|
+
const totalDays = startDayOfWeek + daysInMonth;
|
|
252
|
+
const weeksNeeded = Math.ceil(totalDays / 7);
|
|
253
|
+
const totalCells = weeksNeeded * 7;
|
|
254
|
+
|
|
255
|
+
const days: Dayjs[] = [];
|
|
256
|
+
for (let i = 0; i < totalCells; i++) {
|
|
257
|
+
const date = new Date(startDateCalc);
|
|
258
|
+
date.setDate(startDateCalc.getDate() + i);
|
|
259
|
+
days.push(dayjs(date));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return days;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const days = generateCalendarDays();
|
|
266
|
+
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
267
|
+
|
|
268
|
+
const isDisabled = (date: Dayjs) => {
|
|
269
|
+
if (minDate && date.isBefore(dayjs(minDate), 'day')) return true;
|
|
270
|
+
if (maxDate && date.isAfter(dayjs(maxDate), 'day')) return true;
|
|
271
|
+
|
|
272
|
+
if (mode === 'range' && startDate && !endDate && maxDays) {
|
|
273
|
+
const daysDiff = Math.abs(date.diff(startDate, 'day'));
|
|
274
|
+
if (daysDiff > maxDays) return true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return false;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const rows: Dayjs[][] = [];
|
|
281
|
+
for (let i = 0; i < days.length; i += 7) {
|
|
282
|
+
rows.push(days.slice(i, i + 7));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<View>
|
|
287
|
+
<View className="flex-row">
|
|
288
|
+
{weekDays.map((day, index) => (
|
|
289
|
+
<View key={index} className="flex-1 items-center py-2">
|
|
290
|
+
<Text className="text-xs font-semibold text-slate-500">{day}</Text>
|
|
291
|
+
</View>
|
|
292
|
+
))}
|
|
293
|
+
</View>
|
|
294
|
+
|
|
295
|
+
{rows.map((week, weekIndex) => (
|
|
296
|
+
<View key={weekIndex} className="flex-row">
|
|
297
|
+
{week.map((day, dayIndex) => {
|
|
298
|
+
const isCurrentMonth = day.month() === currentDate.month();
|
|
299
|
+
const isToday = day.isSame(dayjs(), 'day');
|
|
300
|
+
const disabled = isDisabled(day);
|
|
301
|
+
|
|
302
|
+
// Range mode checks
|
|
303
|
+
const isStart = mode === 'range' && startDate && day.isSame(startDate, 'day');
|
|
304
|
+
const isEnd = mode === 'range' && endDate && day.isSame(endDate, 'day');
|
|
305
|
+
const isInRange =
|
|
306
|
+
mode === 'range' &&
|
|
307
|
+
startDate &&
|
|
308
|
+
endDate &&
|
|
309
|
+
day.isBetween(startDate, endDate, 'day', '[]');
|
|
310
|
+
|
|
311
|
+
// Single mode checks
|
|
312
|
+
const isSelected = mode === 'single' && selectedDate && day.isSame(selectedDate, 'day');
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<View key={dayIndex} className="flex-1 p-1">
|
|
316
|
+
<Pressable
|
|
317
|
+
onPress={() => !disabled && onSelect(day)}
|
|
318
|
+
disabled={disabled}
|
|
319
|
+
className={cn(
|
|
320
|
+
'aspect-square items-center justify-center rounded-lg',
|
|
321
|
+
isSelected && 'bg-blue-600',
|
|
322
|
+
isStart && 'bg-blue-600',
|
|
323
|
+
isEnd && 'bg-blue-600',
|
|
324
|
+
isInRange && !isStart && !isEnd && 'bg-blue-100',
|
|
325
|
+
!isSelected &&
|
|
326
|
+
!isStart &&
|
|
327
|
+
!isEnd &&
|
|
328
|
+
!isInRange &&
|
|
329
|
+
isToday &&
|
|
330
|
+
'border border-blue-600',
|
|
331
|
+
!isSelected &&
|
|
332
|
+
!isStart &&
|
|
333
|
+
!isEnd &&
|
|
334
|
+
!isInRange &&
|
|
335
|
+
!isToday &&
|
|
336
|
+
isCurrentMonth &&
|
|
337
|
+
'bg-slate-50',
|
|
338
|
+
disabled && 'opacity-30',
|
|
339
|
+
'web:min-w-10 web:min-h-10'
|
|
340
|
+
|
|
341
|
+
)}
|
|
342
|
+
>
|
|
343
|
+
<Text
|
|
344
|
+
className={cn(
|
|
345
|
+
'text-sm',
|
|
346
|
+
(isSelected || isStart || isEnd) && 'text-white font-semibold',
|
|
347
|
+
!isSelected &&
|
|
348
|
+
!isStart &&
|
|
349
|
+
!isEnd &&
|
|
350
|
+
isInRange &&
|
|
351
|
+
'text-blue-600 font-medium',
|
|
352
|
+
!isSelected &&
|
|
353
|
+
!isStart &&
|
|
354
|
+
!isEnd &&
|
|
355
|
+
!isInRange &&
|
|
356
|
+
isCurrentMonth &&
|
|
357
|
+
'text-slate-900',
|
|
358
|
+
!isSelected &&
|
|
359
|
+
!isStart &&
|
|
360
|
+
!isEnd &&
|
|
361
|
+
!isInRange &&
|
|
362
|
+
!isCurrentMonth &&
|
|
363
|
+
'text-slate-400',
|
|
364
|
+
)}
|
|
365
|
+
>
|
|
366
|
+
{day.format('D')}
|
|
367
|
+
</Text>
|
|
368
|
+
</Pressable>
|
|
369
|
+
</View>
|
|
370
|
+
);
|
|
371
|
+
})}
|
|
372
|
+
</View>
|
|
373
|
+
))}
|
|
374
|
+
</View>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// MonthGrid & YearGrid stay the same...
|
|
379
|
+
function MonthGrid({
|
|
380
|
+
currentDate,
|
|
381
|
+
selectedDate,
|
|
382
|
+
onSelect,
|
|
383
|
+
minDate,
|
|
384
|
+
maxDate,
|
|
385
|
+
}: {
|
|
386
|
+
currentDate: Dayjs;
|
|
387
|
+
selectedDate?: Dayjs;
|
|
388
|
+
onSelect: (monthIndex: number) => void;
|
|
389
|
+
minDate?: Date;
|
|
390
|
+
maxDate?: Date;
|
|
391
|
+
}) {
|
|
392
|
+
const months = Array.from({ length: 12 }, (_, i) => i);
|
|
393
|
+
|
|
394
|
+
const isDisabled = (monthIndex: number) => {
|
|
395
|
+
const date = currentDate.month(monthIndex);
|
|
396
|
+
if (minDate && date.isBefore(dayjs(minDate), 'month')) return true;
|
|
397
|
+
if (maxDate && date.isAfter(dayjs(maxDate), 'month')) return true;
|
|
398
|
+
return false;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
return (
|
|
402
|
+
<View className="flex-row flex-wrap">
|
|
403
|
+
{months.map((monthIndex) => {
|
|
404
|
+
const isSelected =
|
|
405
|
+
selectedDate &&
|
|
406
|
+
monthIndex === selectedDate.month() &&
|
|
407
|
+
currentDate.year() === selectedDate.year();
|
|
408
|
+
const disabled = isDisabled(monthIndex);
|
|
409
|
+
|
|
410
|
+
return (
|
|
411
|
+
<View key={monthIndex} style={{ width: '33.33%' }} className="p-2">
|
|
412
|
+
<Pressable
|
|
413
|
+
onPress={() => !disabled && onSelect(monthIndex)}
|
|
414
|
+
disabled={disabled}
|
|
415
|
+
className={cn(
|
|
416
|
+
'py-4 items-center justify-center rounded-lg',
|
|
417
|
+
isSelected && 'bg-blue-600',
|
|
418
|
+
!isSelected && 'bg-slate-50',
|
|
419
|
+
disabled && 'opacity-30'
|
|
420
|
+
)}
|
|
421
|
+
>
|
|
422
|
+
<Text
|
|
423
|
+
className={cn(
|
|
424
|
+
'text-sm font-medium',
|
|
425
|
+
isSelected && 'text-white',
|
|
426
|
+
!isSelected && 'text-slate-900'
|
|
427
|
+
)}
|
|
428
|
+
>
|
|
429
|
+
{dayjs().month(monthIndex).format('MMM')}
|
|
430
|
+
</Text>
|
|
431
|
+
</Pressable>
|
|
432
|
+
</View>
|
|
433
|
+
);
|
|
434
|
+
})}
|
|
435
|
+
</View>
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function YearGrid({
|
|
440
|
+
currentDate,
|
|
441
|
+
selectedDate,
|
|
442
|
+
onSelect,
|
|
443
|
+
minDate,
|
|
444
|
+
maxDate,
|
|
445
|
+
}: {
|
|
446
|
+
currentDate: Dayjs;
|
|
447
|
+
selectedDate?: Dayjs;
|
|
448
|
+
onSelect: (year: number) => void;
|
|
449
|
+
minDate?: Date;
|
|
450
|
+
maxDate?: Date;
|
|
451
|
+
}) {
|
|
452
|
+
const currentYear = currentDate.year();
|
|
453
|
+
const startYear = currentYear - 6;
|
|
454
|
+
const years = Array.from({ length: 12 }, (_, i) => startYear + i);
|
|
455
|
+
|
|
456
|
+
const isDisabled = (year: number) => {
|
|
457
|
+
const date = currentDate.year(year);
|
|
458
|
+
if (minDate && date.isBefore(dayjs(minDate), 'year')) return true;
|
|
459
|
+
if (maxDate && date.isAfter(dayjs(maxDate), 'year')) return true;
|
|
460
|
+
return false;
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
return (
|
|
464
|
+
<ScrollView showsVerticalScrollIndicator={false} className="max-h-80">
|
|
465
|
+
<View className="flex-row flex-wrap">
|
|
466
|
+
{years.map((year) => {
|
|
467
|
+
const isSelected = selectedDate && year === selectedDate.year();
|
|
468
|
+
const disabled = isDisabled(year);
|
|
469
|
+
|
|
470
|
+
return (
|
|
471
|
+
<View key={year} style={{ width: '33.33%' }} className="p-2">
|
|
472
|
+
<Pressable
|
|
473
|
+
onPress={() => !disabled && onSelect(year)}
|
|
474
|
+
disabled={disabled}
|
|
475
|
+
className={cn(
|
|
476
|
+
'py-4 items-center justify-center rounded-lg',
|
|
477
|
+
isSelected && 'bg-blue-600',
|
|
478
|
+
!isSelected && 'bg-slate-50',
|
|
479
|
+
disabled && 'opacity-30'
|
|
480
|
+
)}
|
|
481
|
+
>
|
|
482
|
+
<Text
|
|
483
|
+
className={cn(
|
|
484
|
+
'text-sm font-medium',
|
|
485
|
+
isSelected && 'text-white',
|
|
486
|
+
!isSelected && 'text-slate-900'
|
|
487
|
+
)}
|
|
488
|
+
>
|
|
489
|
+
{year}
|
|
490
|
+
</Text>
|
|
491
|
+
</Pressable>
|
|
492
|
+
</View>
|
|
493
|
+
);
|
|
494
|
+
})}
|
|
495
|
+
</View>
|
|
496
|
+
</ScrollView>
|
|
497
|
+
);
|
|
498
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { View, Text } from 'react-native';
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
|
|
5
|
+
interface CardProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface CardHeaderProps {
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface CardTitleProps {
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface CardDescriptionProps {
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface CardContentProps {
|
|
26
|
+
children: React.ReactNode;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface CardFooterProps {
|
|
31
|
+
children: React.ReactNode;
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function Card({ children, className }: CardProps) {
|
|
36
|
+
return (
|
|
37
|
+
<View
|
|
38
|
+
className={cn(
|
|
39
|
+
'rounded-lg border border-slate-200 bg-white shadow-sm p-6',
|
|
40
|
+
className
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
{children}
|
|
44
|
+
</View>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function CardHeader({ children, className }: CardHeaderProps) {
|
|
49
|
+
return (
|
|
50
|
+
<View className={cn('flex flex-col gap-1.5', className)}>
|
|
51
|
+
{children}
|
|
52
|
+
</View>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function CardTitle({ children, className }: CardTitleProps) {
|
|
57
|
+
return (
|
|
58
|
+
<Text className={cn('text-2xl font-semibold leading-none tracking-tight text-slate-900', className)}>
|
|
59
|
+
{children}
|
|
60
|
+
</Text>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function CardDescription({ children, className }: CardDescriptionProps) {
|
|
65
|
+
return (
|
|
66
|
+
<Text className={cn('text-sm text-slate-500', className)}>
|
|
67
|
+
{children}
|
|
68
|
+
</Text>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function CardContent({ children, className }: CardContentProps) {
|
|
73
|
+
return (
|
|
74
|
+
<View className={cn('p-0 pt-4', className)}>
|
|
75
|
+
{children}
|
|
76
|
+
</View>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function CardFooter({ children, className }: CardFooterProps) {
|
|
81
|
+
return (
|
|
82
|
+
<View className={cn('flex flex-row items-center p-0 pt-4', className)}>
|
|
83
|
+
{children}
|
|
84
|
+
</View>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// components/ui/checkbox.tsx
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { View, Text, Pressable } from 'react-native';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
|
|
6
|
+
interface CheckboxProps {
|
|
7
|
+
checked?: boolean;
|
|
8
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Checkbox({ checked = false, onCheckedChange, children, className, disabled = false }: CheckboxProps) {
|
|
15
|
+
return (
|
|
16
|
+
<Pressable
|
|
17
|
+
onPress={() => !disabled && onCheckedChange?.(!checked)}
|
|
18
|
+
className={cn('flex-row items-center gap-3', disabled && 'opacity-50', className)}
|
|
19
|
+
disabled={disabled}
|
|
20
|
+
>
|
|
21
|
+
{/* Checkbox Square */}
|
|
22
|
+
<View
|
|
23
|
+
className={cn(
|
|
24
|
+
'h-5 w-5 rounded border-2 items-center justify-center',
|
|
25
|
+
checked ? 'bg-blue-600 border-blue-600' : 'border-slate-300'
|
|
26
|
+
)}
|
|
27
|
+
>
|
|
28
|
+
{checked && (
|
|
29
|
+
// Checkmark icon (simple)
|
|
30
|
+
<Text className="text-white text-xs font-bold">✓</Text>
|
|
31
|
+
)}
|
|
32
|
+
</View>
|
|
33
|
+
|
|
34
|
+
{/* Label */}
|
|
35
|
+
{children}
|
|
36
|
+
</Pressable>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function CheckboxLabel({ children, className }: { children: React.ReactNode; className?: string }) {
|
|
41
|
+
return (
|
|
42
|
+
<Text className={cn('text-base text-slate-900', className)}>
|
|
43
|
+
{children}
|
|
44
|
+
</Text>
|
|
45
|
+
);
|
|
46
|
+
}
|