@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,814 @@
1
+ // components/ui/select-sheet.tsx
2
+ import * as React from 'react';
3
+ import { View, Pressable, ActivityIndicator } from 'react-native';
4
+ import { cn } from '@/lib/utils';
5
+ import { Text } from './text';
6
+ import { Input } from './input';
7
+ import { ChevronDown } from 'lucide-react-native';
8
+ import { useThemeColors } from '@/hooks/useThemeColors';
9
+ import {
10
+ BottomSheet,
11
+ BottomSheetContent,
12
+ BottomSheetHeader,
13
+ BottomSheetTitle,
14
+ BottomSheetList,
15
+ BottomSheetFooter,
16
+ BottomSheetClose,
17
+ } from './bottom-sheet';
18
+ import { Button } from './button';
19
+
20
+ export interface SelectOption {
21
+ label: string;
22
+ value: string | number;
23
+ [key: string]: any;
24
+ }
25
+
26
+ interface BaseSelectProps {
27
+ placeholder?: string;
28
+ label?: string;
29
+ error?: string;
30
+ disabled?: boolean;
31
+ variant?: 'outline' | 'underline';
32
+ size?: 'sm' | 'md' | 'lg';
33
+ className?: string;
34
+ title?: string;
35
+ description?: string;
36
+ snapPoints?: string[];
37
+ searchable?: boolean;
38
+ searchPlaceholder?: string;
39
+ onSearch?: (query: string) => void;
40
+ emptyMessage?: string;
41
+ name?: string;
42
+ }
43
+
44
+ interface SelectSheetProps extends BaseSelectProps {
45
+ options: SelectOption[];
46
+ value?: string | number;
47
+ onValueChange?: (value: string | number) => void;
48
+ onLoadMore?: () => void;
49
+ hasMore?: boolean;
50
+ isLoading?: boolean;
51
+ }
52
+
53
+ interface MultiSelectSheetProps extends BaseSelectProps {
54
+ options: SelectOption[];
55
+ value?: (string | number)[];
56
+ onValueChange?: (value: (string | number)[]) => void;
57
+ maxSelection?: number;
58
+ showCount?: boolean;
59
+ onLoadMore?: () => void;
60
+ hasMore?: boolean;
61
+ isLoading?: boolean;
62
+ }
63
+
64
+ // Single Select
65
+ export function SelectSheet({
66
+ options,
67
+ value,
68
+ onValueChange,
69
+ placeholder = 'Select...',
70
+ label,
71
+ error,
72
+ disabled = false,
73
+ variant = 'outline',
74
+ size = 'md',
75
+ className,
76
+ title = 'Select an option',
77
+ description,
78
+ snapPoints = ['70%'],
79
+ searchable = false,
80
+ searchPlaceholder = 'Search...',
81
+ onSearch,
82
+ emptyMessage = 'No options found',
83
+ onLoadMore,
84
+ hasMore = false,
85
+ isLoading = false,
86
+ name,
87
+ }: SelectSheetProps) {
88
+ const { colors } = useThemeColors();
89
+ const [open, setOpen] = React.useState(false);
90
+ const [searchQuery, setSearchQuery] = React.useState('');
91
+ const flatListRef = React.useRef<any>(null);
92
+
93
+ const selectedOption = options.find((opt) => opt.value === value);
94
+ const displayValue = selectedOption?.label || '';
95
+
96
+ const filteredOptions = React.useMemo(() => {
97
+ if (!searchQuery) return options;
98
+ return options.filter((opt) =>
99
+ opt.label.toLowerCase().includes(searchQuery.toLowerCase())
100
+ );
101
+ }, [options, searchQuery]);
102
+
103
+ const handleSearch = (query: string) => {
104
+ setSearchQuery(query);
105
+ onSearch?.(query);
106
+ };
107
+
108
+ const handleSelect = (selected: SelectOption | SelectOption[] | null) => {
109
+ if (!selected || Array.isArray(selected)) return;
110
+ onValueChange?.(selected.value);
111
+ setOpen(false);
112
+ setSearchQuery('');
113
+ };
114
+
115
+ const handleEndReached = () => {
116
+ if (hasMore && !isLoading && onLoadMore) {
117
+ onLoadMore();
118
+ }
119
+ };
120
+
121
+ const renderFooter = () => {
122
+ if (!isLoading) return null;
123
+ return (
124
+ <View className="py-4 items-center">
125
+ <ActivityIndicator size="small" color={colors.primary} />
126
+ </View>
127
+ );
128
+ };
129
+
130
+ return (
131
+ <View className={cn('w-full', className)}>
132
+ {label && (
133
+ <Text size="sm" variant="label" className="mb-2">
134
+ {label}
135
+ </Text>
136
+ )}
137
+
138
+ <BottomSheet open={open} onOpenChange={setOpen} snapPoints={snapPoints}>
139
+ <Pressable onPress={() => !disabled && setOpen(true)} disabled={disabled}>
140
+ <Input
141
+ value={displayValue}
142
+ placeholder={placeholder}
143
+ editable={false}
144
+ pointerEvents="none"
145
+ variant={variant}
146
+ size={size}
147
+ error={!!error}
148
+ suffix={
149
+ <ChevronDown
150
+ size={20}
151
+ color={disabled ? colors.mutedForeground : colors.foreground}
152
+ />
153
+ }
154
+ containerClassName={cn(disabled && 'opacity-50')}
155
+ />
156
+ </Pressable>
157
+
158
+ <BottomSheetContent>
159
+ <BottomSheetHeader>
160
+ <BottomSheetTitle>{title}</BottomSheetTitle>
161
+ {description && (
162
+ <Text variant="muted" size="sm" className="mt-1">
163
+ {description}
164
+ </Text>
165
+ )}
166
+ </BottomSheetHeader>
167
+
168
+ {searchable && (
169
+ <View className="px-4 pb-3">
170
+ <Input
171
+ value={searchQuery}
172
+ onChangeText={handleSearch}
173
+ placeholder={searchPlaceholder}
174
+ autoFocus
175
+ />
176
+ </View>
177
+ )}
178
+
179
+ {filteredOptions.length === 0 ? (
180
+ <View className="flex-1 items-center justify-center py-8">
181
+ <Text variant="muted">{emptyMessage}</Text>
182
+ </View>
183
+ ) : (
184
+ <BottomSheetList
185
+ data={filteredOptions}
186
+ variant="select"
187
+ selectedValue={value}
188
+ onSelect={handleSelect}
189
+ getItemValue={(item) => item.value}
190
+ onEndReached={handleEndReached}
191
+ onEndReachedThreshold={0.5}
192
+ ListFooterComponent={renderFooter}
193
+ />
194
+ )}
195
+ </BottomSheetContent>
196
+ </BottomSheet>
197
+
198
+ {error && (
199
+ <Text size="sm" className="text-destructive mt-1">
200
+ {error}
201
+ </Text>
202
+ )}
203
+ </View>
204
+ );
205
+ }
206
+
207
+ // Multi Select
208
+ export function MultiSelectSheet({
209
+ options,
210
+ value = [],
211
+ onValueChange,
212
+ placeholder = 'Select...',
213
+ label,
214
+ error,
215
+ disabled = false,
216
+ variant = 'outline',
217
+ size = 'md',
218
+ className,
219
+ title = 'Select options',
220
+ description,
221
+ snapPoints = ['70%'],
222
+ searchable = false,
223
+ searchPlaceholder = 'Search...',
224
+ onSearch,
225
+ emptyMessage = 'No options found',
226
+ maxSelection,
227
+ showCount = true,
228
+ onLoadMore,
229
+ hasMore = false,
230
+ isLoading = false,
231
+ name,
232
+ }: MultiSelectSheetProps) {
233
+ const { colors } = useThemeColors();
234
+ const [open, setOpen] = React.useState(false);
235
+ const [searchQuery, setSearchQuery] = React.useState('');
236
+ const [tempSelected, setTempSelected] = React.useState<(string | number)[]>(value);
237
+ const flatListRef = React.useRef<any>(null);
238
+
239
+ const selectedOptions = options.filter((opt) => value.includes(opt.value));
240
+ const displayValue =
241
+ value.length > 2
242
+ ? `${value.length} items selected`
243
+ : selectedOptions.map((opt) => opt.label).join(', ');
244
+
245
+ const filteredOptions = React.useMemo(() => {
246
+ if (!searchQuery) return options;
247
+ return options.filter((opt) =>
248
+ opt.label.toLowerCase().includes(searchQuery.toLowerCase())
249
+ );
250
+ }, [options, searchQuery]);
251
+
252
+ // Sync tempSelected with value when opening
253
+ React.useEffect(() => {
254
+ if (open) {
255
+ setTempSelected(value);
256
+ }
257
+ }, [open]);
258
+
259
+ const handleSearch = (query: string) => {
260
+ setSearchQuery(query);
261
+ onSearch?.(query);
262
+ };
263
+
264
+ const handleSelect = (selected: SelectOption | SelectOption[] | null) => {
265
+ if (!selected || !Array.isArray(selected)) return;
266
+
267
+ const selectedValues = selected.map((item) => item.value);
268
+
269
+ // Enforce max selection
270
+ if (maxSelection && selectedValues.length > maxSelection) {
271
+ // Take only first N items
272
+ setTempSelected(selectedValues.slice(0, maxSelection));
273
+ return;
274
+ }
275
+
276
+ setTempSelected(selectedValues);
277
+ };
278
+
279
+ const handleConfirm = () => {
280
+ onValueChange?.(tempSelected);
281
+ setOpen(false);
282
+ setSearchQuery('');
283
+ };
284
+
285
+ const handleCancel = () => {
286
+ setTempSelected(value);
287
+ setOpen(false);
288
+ setSearchQuery('');
289
+ };
290
+
291
+ const handleEndReached = () => {
292
+ if (hasMore && !isLoading && onLoadMore) {
293
+ onLoadMore();
294
+ }
295
+ };
296
+
297
+ const renderFooter = () => {
298
+ if (!isLoading) return null;
299
+ return (
300
+ <View className="py-4 items-center">
301
+ <ActivityIndicator size="small" color={colors.primary} />
302
+ </View>
303
+ );
304
+ };
305
+
306
+ return (
307
+ <View className={cn('w-full', className)}>
308
+ {label && (
309
+ <Text size="sm" variant='label' className="mb-2">
310
+ {label}
311
+ </Text>
312
+ )}
313
+
314
+ <BottomSheet open={open} onOpenChange={setOpen} snapPoints={snapPoints}>
315
+ <Pressable onPress={() => !disabled && setOpen(true)} disabled={disabled}>
316
+ <Input
317
+ value={displayValue}
318
+ placeholder={placeholder}
319
+ editable={false}
320
+ pointerEvents="none"
321
+ variant={variant}
322
+ size={size}
323
+ error={!!error}
324
+ suffix={
325
+ <ChevronDown
326
+ size={20}
327
+ color={disabled ? colors.mutedForeground : colors.foreground}
328
+ />
329
+ }
330
+ containerClassName={cn(disabled && 'opacity-50')}
331
+ />
332
+ </Pressable>
333
+
334
+ <BottomSheetContent>
335
+ <BottomSheetHeader>
336
+ <View className="flex-row items-center justify-between">
337
+ <View className="flex-1">
338
+ <BottomSheetTitle>{title}</BottomSheetTitle>
339
+ {description && (
340
+ <Text variant="muted" size="sm" className="mt-1">
341
+ {description}
342
+ </Text>
343
+ )}
344
+ </View>
345
+ {showCount && (
346
+ <Text variant="muted" size="sm">
347
+ {tempSelected.length}
348
+ {maxSelection ? `/${maxSelection}` : ''} selected
349
+ </Text>
350
+ )}
351
+ </View>
352
+ </BottomSheetHeader>
353
+
354
+ {searchable && (
355
+ <View className="px-4 pb-3">
356
+ <Input
357
+ value={searchQuery}
358
+ onChangeText={handleSearch}
359
+ placeholder={searchPlaceholder}
360
+ autoFocus
361
+ />
362
+ </View>
363
+ )}
364
+
365
+ {filteredOptions.length === 0 ? (
366
+ <View className="flex-1 items-center justify-center py-8">
367
+ <Text variant="muted">{emptyMessage}</Text>
368
+ </View>
369
+ ) : (
370
+ <BottomSheetList
371
+ data={filteredOptions}
372
+ variant="multiple"
373
+ selectedValues={tempSelected}
374
+ onSelect={handleSelect}
375
+ getItemValue={(item) => item.value}
376
+ onEndReached={handleEndReached}
377
+ onEndReachedThreshold={0.5}
378
+ ListFooterComponent={renderFooter}
379
+ />
380
+ )}
381
+
382
+ <BottomSheetFooter>
383
+ <View className="flex-row gap-2">
384
+ <BottomSheetClose>
385
+ <Button
386
+ variant="outline"
387
+ onPress={handleCancel}
388
+ className="flex-1"
389
+ >
390
+ Cancel
391
+ </Button>
392
+ </BottomSheetClose>
393
+ <Button onPress={handleConfirm} className="flex-1">
394
+ Confirm
395
+ </Button>
396
+ </View>
397
+ </BottomSheetFooter>
398
+ </BottomSheetContent>
399
+ </BottomSheet>
400
+
401
+ {error && (
402
+ <Text size="sm" className="text-destructive mt-1">
403
+ {error}
404
+ </Text>
405
+ )}
406
+ </View>
407
+ );
408
+ }
409
+
410
+ // // components/ui/select-sheet.tsx
411
+ // import * as React from 'react';
412
+ // import { View, Pressable, ActivityIndicator } from 'react-native';
413
+ // import { cn } from '@/lib/utils';
414
+ // import { Text } from './text';
415
+ // import { Input } from './input';
416
+ // import { ChevronDown } from 'lucide-react-native';
417
+ // import { useThemeColors } from '@/hooks/useThemeColors';
418
+ // import {
419
+ // BottomSheet,
420
+ // BottomSheetContent,
421
+ // BottomSheetHeader,
422
+ // BottomSheetTitle,
423
+ // BottomSheetList,
424
+ // BottomSheetFooter,
425
+ // BottomSheetClose,
426
+ // } from './bottom-sheet';
427
+ // import { Button } from './button';
428
+
429
+ // export interface SelectOption {
430
+ // label: string;
431
+ // value: string | number;
432
+ // [key: string]: any;
433
+ // }
434
+
435
+ // interface BaseSelectProps {
436
+ // placeholder?: string;
437
+ // label?: string;
438
+ // error?: string;
439
+ // disabled?: boolean;
440
+ // variant?: 'outline' | 'underline';
441
+ // size?: 'sm' | 'md' | 'lg';
442
+ // className?: string;
443
+ // title?: string;
444
+ // description?: string;
445
+ // snapPoints?: string[];
446
+ // searchable?: boolean;
447
+ // searchPlaceholder?: string;
448
+ // onSearch?: (query: string) => void;
449
+ // emptyMessage?: string;
450
+ // name?: string;
451
+ // }
452
+
453
+ // interface SelectSheetProps extends BaseSelectProps {
454
+ // options: SelectOption[];
455
+ // value?: string | number;
456
+ // onValueChange?: (value: string | number) => void;
457
+ // onLoadMore?: () => void;
458
+ // hasMore?: boolean;
459
+ // isLoading?: boolean;
460
+ // }
461
+
462
+ // interface MultiSelectSheetProps extends BaseSelectProps {
463
+ // options: SelectOption[];
464
+ // value?: (string | number)[];
465
+ // onValueChange?: (value: (string | number)[]) => void;
466
+ // maxSelection?: number;
467
+ // showCount?: boolean;
468
+ // onLoadMore?: () => void;
469
+ // hasMore?: boolean;
470
+ // isLoading?: boolean;
471
+ // }
472
+
473
+ // // Single Select
474
+ // export function SelectSheet({
475
+ // options,
476
+ // value,
477
+ // onValueChange,
478
+ // placeholder = 'Select...',
479
+ // label,
480
+ // error,
481
+ // disabled = false,
482
+ // variant = 'outline',
483
+ // size = 'md',
484
+ // className,
485
+ // title = 'Select an option',
486
+ // description,
487
+ // snapPoints = ['70%'],
488
+ // searchable = false,
489
+ // searchPlaceholder = 'Search...',
490
+ // onSearch,
491
+ // emptyMessage = 'No options found',
492
+ // onLoadMore,
493
+ // hasMore = false,
494
+ // isLoading = false,
495
+ // name,
496
+ // }: SelectSheetProps) {
497
+ // const { colors } = useThemeColors();
498
+ // const [open, setOpen] = React.useState(false);
499
+ // const [searchQuery, setSearchQuery] = React.useState('');
500
+
501
+ // const selectedOption = options.find((opt) => opt.value === value);
502
+ // const displayValue = selectedOption?.label || '';
503
+
504
+ // const filteredOptions = React.useMemo(() => {
505
+ // if (!searchQuery) return options;
506
+ // return options.filter((opt) =>
507
+ // opt.label.toLowerCase().includes(searchQuery.toLowerCase())
508
+ // );
509
+ // }, [options, searchQuery]);
510
+
511
+ // const handleSearch = (query: string) => {
512
+ // setSearchQuery(query);
513
+ // onSearch?.(query);
514
+ // };
515
+
516
+ // const handleSelect = (selected: SelectOption | SelectOption[] | null) => {
517
+ // if (!selected || Array.isArray(selected)) return;
518
+ // onValueChange?.(selected.value);
519
+ // setOpen(false);
520
+ // setSearchQuery('');
521
+ // };
522
+
523
+ // const handleEndReached = () => {
524
+ // if (hasMore && !isLoading && onLoadMore) {
525
+ // onLoadMore();
526
+ // }
527
+ // };
528
+
529
+ // const renderFooter = () => {
530
+ // if (!isLoading) return null;
531
+ // return (
532
+ // <View className="py-4 items-center">
533
+ // <ActivityIndicator size="small" color={colors.primary} />
534
+ // </View>
535
+ // );
536
+ // };
537
+
538
+ // return (
539
+ // <View className={cn('w-full', className)}>
540
+ // {label && (
541
+ // <Text size="sm" variant="label" className="mb-2">
542
+ // {label}
543
+ // </Text>
544
+ // )}
545
+
546
+ // <BottomSheet open={open} onOpenChange={setOpen} snapPoints={snapPoints}>
547
+ // <Pressable onPress={() => !disabled && setOpen(true)} disabled={disabled}>
548
+ // <Input
549
+ // value={displayValue}
550
+ // placeholder={placeholder}
551
+ // editable={false}
552
+ // pointerEvents="none"
553
+ // variant={variant}
554
+ // size={size}
555
+ // error={!!error}
556
+ // suffix={
557
+ // <ChevronDown
558
+ // size={20}
559
+ // color={disabled ? colors.mutedForeground : colors.foreground}
560
+ // />
561
+ // }
562
+ // containerClassName={cn(disabled && 'opacity-50')}
563
+ // />
564
+ // </Pressable>
565
+
566
+ // <BottomSheetContent>
567
+ // <BottomSheetHeader>
568
+ // <BottomSheetTitle>{title}</BottomSheetTitle>
569
+ // {description && (
570
+ // <Text variant="muted" size="sm" className="mt-1">
571
+ // {description}
572
+ // </Text>
573
+ // )}
574
+ // </BottomSheetHeader>
575
+
576
+ // {searchable && (
577
+ // <View className="px-4 pb-3">
578
+ // <Input
579
+ // value={searchQuery}
580
+ // onChangeText={handleSearch}
581
+ // placeholder={searchPlaceholder}
582
+ // autoFocus
583
+ // />
584
+ // </View>
585
+ // )}
586
+
587
+ // {filteredOptions.length === 0 ? (
588
+ // <View className="flex-1 items-center justify-center py-8">
589
+ // <Text variant="muted">{emptyMessage}</Text>
590
+ // </View>
591
+ // ) : (
592
+ // <BottomSheetList
593
+ // data={filteredOptions}
594
+ // variant="select"
595
+ // selectedValue={value}
596
+ // onSelect={handleSelect}
597
+ // getItemValue={(item) => item.value}
598
+ // onEndReached={handleEndReached}
599
+ // onEndReachedThreshold={0.5}
600
+ // ListFooterComponent={renderFooter}
601
+ // />
602
+ // )}
603
+ // {/* <BottomSheetFooter>
604
+ // {renderFooter()}
605
+ // </BottomSheetFooter> */}
606
+ // </BottomSheetContent>
607
+ // </BottomSheet>
608
+
609
+ // {error && (
610
+ // <Text size="sm" className="text-destructive mt-1">
611
+ // {error}
612
+ // </Text>
613
+ // )}
614
+ // </View>
615
+ // );
616
+ // }
617
+
618
+ // // Multi Select
619
+ // export function MultiSelectSheet({
620
+ // options,
621
+ // value = [],
622
+ // onValueChange,
623
+ // placeholder = 'Select...',
624
+ // label,
625
+ // error,
626
+ // disabled = false,
627
+ // variant = 'outline',
628
+ // size = 'md',
629
+ // className,
630
+ // title = 'Select options',
631
+ // description,
632
+ // snapPoints = ['70%'],
633
+ // searchable = false,
634
+ // searchPlaceholder = 'Search...',
635
+ // onSearch,
636
+ // emptyMessage = 'No options found',
637
+ // maxSelection,
638
+ // showCount = true,
639
+ // onLoadMore,
640
+ // hasMore = false,
641
+ // isLoading = false,
642
+ // name,
643
+ // }: MultiSelectSheetProps) {
644
+ // const { colors } = useThemeColors();
645
+ // const [open, setOpen] = React.useState(false);
646
+ // const [searchQuery, setSearchQuery] = React.useState('');
647
+ // const [tempSelected, setTempSelected] = React.useState<(string | number)[]>(value);
648
+
649
+ // const selectedOptions = options.filter((opt) => tempSelected.includes(opt.value));
650
+ // const displayValue =
651
+ // tempSelected.length > 2
652
+ // ? `${tempSelected.length} items selected`
653
+ // : selectedOptions.map((opt) => opt.label).join(', ');
654
+
655
+ // const filteredOptions = React.useMemo(() => {
656
+ // if (!searchQuery) return options;
657
+ // return options.filter((opt) =>
658
+ // opt.label.toLowerCase().includes(searchQuery.toLowerCase())
659
+ // );
660
+ // }, [options, searchQuery]);
661
+
662
+ // React.useEffect(() => {
663
+ // if (open) {
664
+ // setTempSelected(value);
665
+ // }
666
+ // }, [open, value]);
667
+
668
+ // const handleSearch = (query: string) => {
669
+ // setSearchQuery(query);
670
+ // onSearch?.(query);
671
+ // };
672
+
673
+ // const handleSelect = (selected: SelectOption | SelectOption[] | null) => {
674
+ // if (!selected || !Array.isArray(selected)) return;
675
+
676
+ // const selectedValues = selected.map((item) => item.value);
677
+
678
+ // if (maxSelection && selectedValues.length > maxSelection) {
679
+ // return;
680
+ // }
681
+
682
+ // setTempSelected(selectedValues);
683
+ // };
684
+
685
+ // const handleConfirm = () => {
686
+ // onValueChange?.(tempSelected);
687
+ // setOpen(false);
688
+ // setSearchQuery('');
689
+ // };
690
+
691
+ // const handleCancel = () => {
692
+ // setTempSelected(value);
693
+ // setOpen(false);
694
+ // setSearchQuery('');
695
+ // };
696
+
697
+ // const handleEndReached = () => {
698
+ // if (hasMore && !isLoading && onLoadMore) {
699
+ // onLoadMore();
700
+ // }
701
+ // };
702
+
703
+ // const renderFooter = () => {
704
+ // if (!isLoading) return null;
705
+ // return (
706
+ // <View className="py-4 items-center">
707
+ // <ActivityIndicator size="small" color={colors.primary} />
708
+ // </View>
709
+ // );
710
+ // };
711
+
712
+ // return (
713
+ // <View className={cn('w-full', className)}>
714
+ // {label && (
715
+ // <Text size="sm" variant="label" className="mb-2">
716
+ // {label}
717
+ // </Text>
718
+ // )}
719
+
720
+ // <BottomSheet open={open} onOpenChange={setOpen} snapPoints={snapPoints}>
721
+ // <Pressable onPress={() => !disabled && setOpen(true)} disabled={disabled}>
722
+ // <Input
723
+ // value={displayValue}
724
+ // placeholder={placeholder}
725
+ // editable={false}
726
+ // pointerEvents="none"
727
+ // variant={variant}
728
+ // size={size}
729
+ // error={!!error}
730
+ // suffix={
731
+ // <ChevronDown
732
+ // size={20}
733
+ // color={disabled ? colors.mutedForeground : colors.foreground}
734
+ // />
735
+ // }
736
+ // containerClassName={cn(disabled && 'opacity-50')}
737
+ // />
738
+ // </Pressable>
739
+
740
+ // <BottomSheetContent>
741
+ // <BottomSheetHeader>
742
+ // <View className="flex-row items-center justify-between">
743
+ // <View className="flex-1">
744
+ // <BottomSheetTitle>{title}</BottomSheetTitle>
745
+ // {description && (
746
+ // <Text variant="muted" size="sm" className="mt-1">
747
+ // {description}
748
+ // </Text>
749
+ // )}
750
+ // </View>
751
+ // {showCount && (
752
+ // <Text variant="muted" size="sm">
753
+ // {tempSelected.length}
754
+ // {maxSelection ? `/${maxSelection}` : ''} selected
755
+ // </Text>
756
+ // )}
757
+ // </View>
758
+ // </BottomSheetHeader>
759
+
760
+ // {searchable && (
761
+ // <View className="px-4 pb-3">
762
+ // <Input
763
+ // value={searchQuery}
764
+ // onChangeText={handleSearch}
765
+ // placeholder={searchPlaceholder}
766
+ // autoFocus
767
+ // />
768
+ // </View>
769
+ // )}
770
+
771
+ // {filteredOptions.length === 0 ? (
772
+ // <View className="flex-1 items-center justify-center py-8">
773
+ // <Text variant="muted">{emptyMessage}</Text>
774
+ // </View>
775
+ // ) : (
776
+ // <BottomSheetList
777
+ // data={filteredOptions}
778
+ // variant="multiple"
779
+ // selectedValues={tempSelected}
780
+ // onSelect={handleSelect}
781
+ // getItemValue={(item) => item.value}
782
+ // onEndReached={handleEndReached}
783
+ // onEndReachedThreshold={0.5}
784
+ // ListFooterComponent={renderFooter}
785
+ // />
786
+ // )}
787
+
788
+ // <BottomSheetFooter>
789
+ // <View className="flex-row gap-2">
790
+ // <BottomSheetClose>
791
+ // <Button
792
+ // variant="outline"
793
+ // onPress={handleCancel}
794
+ // className="flex-1"
795
+ // >
796
+ // Cancel
797
+ // </Button>
798
+ // </BottomSheetClose>
799
+ // <Button onPress={handleConfirm} className="flex-1">
800
+ // Confirm
801
+ // </Button>
802
+ // </View>
803
+ // </BottomSheetFooter>
804
+ // </BottomSheetContent>
805
+ // </BottomSheet>
806
+
807
+ // {error && (
808
+ // <Text size="sm" className="text-destructive mt-1">
809
+ // {error}
810
+ // </Text>
811
+ // )}
812
+ // </View>
813
+ // );
814
+ // }