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