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