@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,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
+ }