@umituz/react-native-bottom-sheet 1.3.4 → 1.4.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.
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Presentation - Filter Bottom Sheet component
3
+ *
4
+ * Advanced filtering UI using @umituz/react-native-bottom-sheet
5
+ */
6
+
7
+ import React, { forwardRef, useCallback } from 'react';
8
+ import { View, StyleSheet, TouchableOpacity, ScrollView } from 'react-native';
9
+ import { BottomSheetModal } from '../BottomSheetModal';
10
+ import type { BottomSheetModalRef } from '../../../domain/entities/BottomSheet';
11
+ import { AtomicText, AtomicIcon, AtomicButton } from '@umituz/react-native-design-system';
12
+ import { useAppDesignTokens } from '@umituz/react-native-design-system';
13
+ import { useLocalization } from '@umituz/react-native-localization';
14
+ import type { FilterOption, FilterCategory } from '../../../domain/entities/Filter';
15
+ import { FilterUtils } from '../../../domain/entities/Filter';
16
+
17
+ export interface FilterBottomSheetProps {
18
+ readonly categories: FilterCategory[];
19
+ readonly selectedIds: string[];
20
+ readonly onFilterPress: (id: string, categoryId: string) => void;
21
+ readonly onClearFilters: () => void;
22
+ readonly onDismiss?: () => void;
23
+ readonly title?: string;
24
+ readonly defaultId?: string;
25
+ }
26
+
27
+ export const FilterBottomSheet = forwardRef<BottomSheetModalRef, FilterBottomSheetProps>(({
28
+ categories,
29
+ selectedIds,
30
+ onFilterPress,
31
+ onClearFilters,
32
+ onDismiss,
33
+ title,
34
+ defaultId = 'all'
35
+ }, ref) => {
36
+ const tokens = useAppDesignTokens();
37
+ const { t } = useLocalization();
38
+
39
+ const styles = React.useMemo(() => StyleSheet.create({
40
+ container: {
41
+ flex: 1,
42
+ padding: 16,
43
+ },
44
+ header: {
45
+ flexDirection: 'row',
46
+ justifyContent: 'space-between',
47
+ alignItems: 'center',
48
+ marginBottom: 20,
49
+ },
50
+ category: {
51
+ marginBottom: 24,
52
+ },
53
+ categoryTitle: {
54
+ marginBottom: 12,
55
+ opacity: 0.7,
56
+ },
57
+ optionsGrid: {
58
+ flexDirection: 'row',
59
+ flexWrap: 'wrap',
60
+ gap: 8,
61
+ },
62
+ option: {
63
+ flexDirection: 'row',
64
+ alignItems: 'center',
65
+ paddingHorizontal: 12,
66
+ paddingVertical: 8,
67
+ borderRadius: 20,
68
+ backgroundColor: tokens.colors.surfaceVariant,
69
+ gap: 6,
70
+ borderWidth: 1,
71
+ borderColor: 'transparent',
72
+ },
73
+ optionLeft: {
74
+ flexDirection: 'row',
75
+ alignItems: 'center',
76
+ gap: 6,
77
+ },
78
+ footer: {
79
+ marginTop: 16,
80
+ paddingBottom: 8,
81
+ }
82
+ }), [tokens]);
83
+
84
+ const renderOption = useCallback((option: FilterOption, categoryId: string) => {
85
+ const isSelected = selectedIds.includes(option.id);
86
+
87
+ return (
88
+ <TouchableOpacity
89
+ key={option.id}
90
+ style={[
91
+ styles.option,
92
+ isSelected && { backgroundColor: tokens.colors.primary + '15' }
93
+ ]}
94
+ onPress={() => onFilterPress(option.id, categoryId)}
95
+ >
96
+ <View style={styles.optionLeft}>
97
+ {option.icon && (
98
+ <AtomicIcon
99
+ name={option.icon as any}
100
+ size="sm"
101
+ color={isSelected ? 'primary' : 'secondary'}
102
+ />
103
+ )}
104
+ <AtomicText
105
+ type="bodyMedium"
106
+ style={[isSelected && { color: tokens.colors.primary, fontWeight: '600' }]}
107
+ >
108
+ {option.label}
109
+ </AtomicText>
110
+ </View>
111
+ {isSelected && (
112
+ <AtomicIcon name="checkmark" size="sm" color="primary" />
113
+ )}
114
+ </TouchableOpacity>
115
+ );
116
+ }, [selectedIds, tokens, onFilterPress]);
117
+
118
+ const renderCategory = useCallback((category: FilterCategory) => (
119
+ <View key={category.id} style={styles.category}>
120
+ <AtomicText type="labelLarge" style={styles.categoryTitle}>
121
+ {category.title}
122
+ </AtomicText>
123
+ <View style={styles.optionsGrid}>
124
+ {category.options.map(option => renderOption(option, category.id))}
125
+ </View>
126
+ </View>
127
+ ), [renderOption, styles]);
128
+
129
+
130
+ const hasActiveFilters = FilterUtils.hasActiveFilter(selectedIds, defaultId);
131
+
132
+ return (
133
+ <BottomSheetModal
134
+ ref={ref}
135
+ preset="medium"
136
+ onDismiss={onDismiss}
137
+ backgroundColor={tokens.colors.surface}
138
+ >
139
+ <View style={styles.container}>
140
+ <View style={styles.header}>
141
+ <AtomicText type="headlineSmall">{title || t('common.filter')}</AtomicText>
142
+ {hasActiveFilters && (
143
+ <TouchableOpacity onPress={onClearFilters}>
144
+ <AtomicText type="labelLarge" color="error">{t('common.clear')}</AtomicText>
145
+ </TouchableOpacity>
146
+ )}
147
+ </View>
148
+
149
+ <ScrollView showsVerticalScrollIndicator={false}>
150
+ {categories.map(renderCategory)}
151
+ </ScrollView>
152
+
153
+ <View style={styles.footer}>
154
+ <AtomicButton
155
+ onPress={() => (ref as any).current?.dismiss()}
156
+ fullWidth
157
+ >
158
+ {t('common.apply')}
159
+ </AtomicButton>
160
+ </View>
161
+ </View>
162
+ </BottomSheetModal>
163
+ );
164
+ });
165
+
166
+ FilterBottomSheet.displayName = 'FilterBottomSheet';
@@ -0,0 +1,115 @@
1
+ import React, { useCallback } from "react";
2
+ import { View, StyleSheet, ScrollView, Modal, Pressable } from "react-native";
3
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
4
+ import { AtomicButton, useAppDesignTokens } from "@umituz/react-native-design-system";
5
+ import { useLocalization } from "@umituz/react-native-localization";
6
+ import type { FilterOption } from "../../../domain/entities/Filter";
7
+ import { FilterUtils } from "../../../domain/entities/Filter";
8
+ import { FilterSheetHeader } from "./FilterSheetComponents/FilterSheetHeader";
9
+ import { FilterSheetOption } from "./FilterSheetComponents/FilterSheetOption";
10
+
11
+ export interface FilterSheetProps {
12
+ visible: boolean;
13
+ options: FilterOption[];
14
+ selectedIds: string[];
15
+ onFilterPress: (filterId: string) => void;
16
+ onClearFilters: () => void;
17
+ onClose?: () => void;
18
+ defaultFilterId?: string;
19
+ title?: string;
20
+ }
21
+
22
+ export const FilterSheet: React.FC<FilterSheetProps> = ({
23
+ visible,
24
+ options,
25
+ selectedIds,
26
+ onFilterPress,
27
+ onClearFilters,
28
+ onClose,
29
+ defaultFilterId = "all",
30
+ title,
31
+ }) => {
32
+ const tokens = useAppDesignTokens();
33
+ const { t } = useLocalization();
34
+ const insets = useSafeAreaInsets();
35
+
36
+ const hasActiveFilter = FilterUtils.hasActiveFilter(selectedIds, defaultFilterId);
37
+
38
+ const handleFilterPressWithClose = useCallback((id: string) => {
39
+ onFilterPress(id);
40
+ onClose?.();
41
+ }, [onFilterPress, onClose]);
42
+
43
+ return (
44
+ <Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
45
+ <Pressable style={styles.backdrop} onPress={onClose}>
46
+ <Pressable
47
+ style={[styles.sheet, { backgroundColor: tokens.colors.surface, paddingBottom: insets.bottom }]}
48
+ onPress={(e) => e.stopPropagation()}
49
+ >
50
+ <View style={[styles.handle, { backgroundColor: tokens.colors.border }]} />
51
+
52
+ <FilterSheetHeader
53
+ title={title || t("general.filter")}
54
+ onClose={() => onClose?.()}
55
+ tokens={tokens}
56
+ />
57
+
58
+ <ScrollView style={styles.optionsList} showsVerticalScrollIndicator={false}>
59
+ {options.map((option) => (
60
+ <FilterSheetOption
61
+ key={option.id}
62
+ option={option}
63
+ isSelected={selectedIds.includes(option.id)}
64
+ onPress={handleFilterPressWithClose}
65
+ tokens={tokens}
66
+ />
67
+ ))}
68
+ </ScrollView>
69
+
70
+ {hasActiveFilter && (
71
+ <View style={[styles.footer, { borderTopColor: tokens.colors.border, borderTopWidth: tokens.borders.width.thin }]}>
72
+ <AtomicButton variant="outline" onPress={onClearFilters} fullWidth>
73
+ {t("general.clear")}
74
+ </AtomicButton>
75
+ </View>
76
+ )}
77
+ </Pressable>
78
+ </Pressable>
79
+ </Modal>
80
+ );
81
+ };
82
+
83
+ FilterSheet.displayName = "FilterSheet";
84
+
85
+ const styles = StyleSheet.create({
86
+ backdrop: {
87
+ flex: 1,
88
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
89
+ justifyContent: "flex-end",
90
+ },
91
+ sheet: {
92
+ borderTopLeftRadius: 16,
93
+ borderTopRightRadius: 16,
94
+ maxHeight: "80%",
95
+ },
96
+ handle: {
97
+ width: 40,
98
+ height: 4,
99
+ borderRadius: 2,
100
+ alignSelf: "center",
101
+ marginTop: 8,
102
+ marginBottom: 8,
103
+ },
104
+ optionsList: {
105
+ maxHeight: 400,
106
+ paddingVertical: 8,
107
+ },
108
+ footer: {
109
+ paddingHorizontal: 20,
110
+ paddingTop: 16,
111
+ paddingBottom: 8,
112
+ },
113
+ });
114
+
115
+
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import { View, StyleSheet, TouchableOpacity } from 'react-native';
3
+ import { AtomicText, AtomicIcon } from '@umituz/react-native-design-system';
4
+ import type { useAppDesignTokens } from '@umituz/react-native-design-system';
5
+
6
+ interface FilterSheetHeaderProps {
7
+ title: string;
8
+ onClose: () => void;
9
+ tokens: ReturnType<typeof useAppDesignTokens>;
10
+ }
11
+
12
+ export const FilterSheetHeader = ({ title, onClose, tokens }: FilterSheetHeaderProps) => (
13
+ <View style={[styles.header, { borderBottomColor: tokens.colors.border, borderBottomWidth: tokens.borders.width.thin }]}>
14
+ <AtomicText type="headlineMedium" style={styles.title}>{title}</AtomicText>
15
+ <TouchableOpacity onPress={onClose} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
16
+ <AtomicIcon name="close" size="md" color="primary" />
17
+ </TouchableOpacity>
18
+ </View>
19
+ );
20
+
21
+ const styles = StyleSheet.create({
22
+ header: {
23
+ flexDirection: 'row',
24
+ alignItems: 'center',
25
+ justifyContent: 'space-between',
26
+ paddingHorizontal: 20,
27
+ paddingBottom: 16,
28
+ paddingTop: 8,
29
+ },
30
+ title: {
31
+ fontWeight: '600',
32
+ },
33
+ });
@@ -0,0 +1,59 @@
1
+ import React from 'react';
2
+ import { View, StyleSheet, TouchableOpacity } from 'react-native';
3
+ import { AtomicText, AtomicIcon } from '@umituz/react-native-design-system';
4
+ import type { useAppDesignTokens } from '@umituz/react-native-design-system';
5
+ import type { FilterOption } from '../../../../domain/entities/Filter';
6
+
7
+ interface FilterSheetOptionProps {
8
+ option: FilterOption;
9
+ isSelected: boolean;
10
+ onPress: (id: string) => void;
11
+ tokens: ReturnType<typeof useAppDesignTokens>;
12
+ }
13
+
14
+ export const FilterSheetOption = ({ option, isSelected, onPress, tokens }: FilterSheetOptionProps) => (
15
+ <TouchableOpacity
16
+ onPress={() => onPress(option.id)}
17
+ style={[
18
+ styles.option,
19
+ { borderBottomColor: tokens.colors.borderLight, borderBottomWidth: tokens.borders.width.thin },
20
+ isSelected && { backgroundColor: tokens.colors.primary + '15' }
21
+ ]}
22
+ >
23
+ <View style={styles.optionContent}>
24
+ {option.icon && (
25
+ <AtomicIcon
26
+ name={option.icon as any}
27
+ size="md"
28
+ color={isSelected ? 'primary' : 'secondary'}
29
+ />
30
+ )}
31
+ <AtomicText
32
+ type="bodyLarge"
33
+ style={[styles.optionLabel, isSelected && { color: tokens.colors.primary, fontWeight: '600' }]}
34
+ >
35
+ {option.label}
36
+ </AtomicText>
37
+ </View>
38
+ {isSelected && <AtomicIcon name="checkmark-circle" size="md" color="primary" />}
39
+ </TouchableOpacity>
40
+ );
41
+
42
+ const styles = StyleSheet.create({
43
+ option: {
44
+ flexDirection: 'row',
45
+ alignItems: 'center',
46
+ justifyContent: 'space-between',
47
+ paddingHorizontal: 20,
48
+ paddingVertical: 16,
49
+ },
50
+ optionContent: {
51
+ flexDirection: 'row',
52
+ alignItems: 'center',
53
+ gap: 12,
54
+ flex: 1,
55
+ },
56
+ optionLabel: {
57
+ flex: 1,
58
+ },
59
+ });
@@ -1,133 +1,16 @@
1
- /**
2
- * useBottomSheet Hook
3
- *
4
- * React hook for managing bottom sheet state and interactions.
5
- * Provides a clean API for common bottom sheet operations.
6
- *
7
- * Features:
8
- * - Open/close bottom sheet
9
- * - Snap to specific index
10
- * - Expand to max height
11
- * - Collapse to min height
12
- * - Track current position
13
- * - Preset configurations
14
- *
15
- * Usage:
16
- * ```tsx
17
- * const { sheetRef, open, close, expand, collapse } = useBottomSheet();
18
- *
19
- * return (
20
- * <>
21
- * <Button onPress={open}>Open Sheet</Button>
22
- * <BottomSheet ref={sheetRef} preset="medium">
23
- * <Text>Content</Text>
24
- * </BottomSheet>
25
- * </>
26
- * );
27
- * ```
28
- */
29
-
30
1
  import { useRef, useCallback } from 'react';
31
- import type { BottomSheetRef } from '../components/BottomSheet';
32
-
33
- /**
34
- * Return type for useBottomSheet hook
35
- */
36
- export interface UseBottomSheetReturn {
37
- /**
38
- * Ref to attach to BottomSheet component
39
- */
40
- sheetRef: React.RefObject<BottomSheetRef | null>;
41
-
42
- /**
43
- * Open bottom sheet to initial index
44
- */
45
- open: () => void;
46
-
47
- /**
48
- * Close bottom sheet completely
49
- */
50
- close: () => void;
51
-
52
- /**
53
- * Expand to maximum height
54
- */
55
- expand: () => void;
56
-
57
- /**
58
- * Collapse to minimum height
59
- */
60
- collapse: () => void;
61
-
62
- /**
63
- * Snap to specific index
64
- */
65
- snapToIndex: (index: number) => void;
2
+ import type { BottomSheetRef, UseBottomSheetReturn } from '../../domain/entities/BottomSheet';
66
3
 
67
- /**
68
- * Snap to specific position
69
- */
70
- snapToPosition: (position: string | number) => void;
71
- }
72
-
73
- /**
74
- * useBottomSheet Hook
75
- *
76
- * Hook for managing bottom sheet state and interactions.
77
- * Provides imperative methods for controlling the sheet.
78
- */
79
4
  export const useBottomSheet = (): UseBottomSheetReturn => {
80
5
  const sheetRef = useRef<BottomSheetRef>(null);
81
6
 
82
- /**
83
- * Open bottom sheet to first snap point
84
- */
85
- const open = useCallback(() => {
86
- sheetRef.current?.snapToIndex(0);
87
- }, []);
88
-
89
- /**
90
- * Close bottom sheet completely
91
- */
92
- const close = useCallback(() => {
93
- sheetRef.current?.close();
94
- }, []);
95
-
96
- /**
97
- * Expand to maximum height (last snap point)
98
- */
99
- const expand = useCallback(() => {
100
- sheetRef.current?.expand();
101
- }, []);
102
-
103
- /**
104
- * Collapse to minimum height (first snap point)
105
- */
106
- const collapse = useCallback(() => {
107
- sheetRef.current?.collapse();
108
- }, []);
7
+ const open = useCallback(() => sheetRef.current?.snapToIndex(0), []);
8
+ const close = useCallback(() => sheetRef.current?.close(), []);
9
+ const expand = useCallback(() => sheetRef.current?.expand(), []);
10
+ const collapse = useCallback(() => sheetRef.current?.collapse(), []);
11
+ const snapToIndex = useCallback((index: number) => sheetRef.current?.snapToIndex(index), []);
12
+ const snapToPosition = useCallback((pos: string | number) => sheetRef.current?.snapToPosition(pos), []);
109
13
 
110
- /**
111
- * Snap to specific index
112
- */
113
- const snapToIndex = useCallback((index: number) => {
114
- sheetRef.current?.snapToIndex(index);
115
- }, []);
116
-
117
- /**
118
- * Snap to specific position (percentage or fixed)
119
- */
120
- const snapToPosition = useCallback((position: string | number) => {
121
- sheetRef.current?.snapToPosition(position);
122
- }, []);
123
-
124
- return {
125
- sheetRef,
126
- open,
127
- close,
128
- expand,
129
- collapse,
130
- snapToIndex,
131
- snapToPosition,
132
- };
14
+ return { sheetRef, open, close, expand, collapse, snapToIndex, snapToPosition };
133
15
  };
16
+
@@ -1,134 +1,17 @@
1
- /**
2
- * useBottomSheetModal Hook
3
- *
4
- * React hook for managing bottom sheet modal state and interactions.
5
- * Provides a clean API for common bottom sheet modal operations.
6
- *
7
- * Features:
8
- * - Present/dismiss bottom sheet modal
9
- * - Snap to specific index
10
- * - Expand to max height
11
- * - Collapse to min height
12
- * - Track current position
13
- * - Preset configurations
14
- *
15
- * Usage:
16
- * ```tsx
17
- * const { modalRef, present, dismiss, expand, collapse } = useBottomSheetModal();
18
- *
19
- * return (
20
- * <>
21
- * <Button onPress={present}>Open Modal</Button>
22
- * <BottomSheetModal ref={modalRef} preset="medium">
23
- * <Text>Content</Text>
24
- * </BottomSheetModal>
25
- * </>
26
- * );
27
- * ```
28
- */
29
-
30
1
  import { useRef, useCallback } from 'react';
31
- import type { BottomSheetModalRef } from '../components/BottomSheetModal';
32
-
33
- /**
34
- * Return type for useBottomSheetModal hook
35
- */
36
- export interface UseBottomSheetModalReturn {
37
- /**
38
- * Ref to attach to BottomSheetModal component
39
- */
40
- modalRef: React.RefObject<BottomSheetModalRef | null>;
41
-
42
- /**
43
- * Present bottom sheet modal
44
- */
45
- present: () => void;
46
-
47
- /**
48
- * Dismiss bottom sheet modal
49
- */
50
- dismiss: () => void;
51
-
52
- /**
53
- * Expand to maximum height
54
- */
55
- expand: () => void;
56
-
57
- /**
58
- * Collapse to minimum height
59
- */
60
- collapse: () => void;
61
-
62
- /**
63
- * Snap to specific index
64
- */
65
- snapToIndex: (index: number) => void;
2
+ import type { BottomSheetModalRef, UseBottomSheetModalReturn } from '../../domain/entities/BottomSheet';
66
3
 
67
- /**
68
- * Snap to specific position
69
- */
70
- snapToPosition: (position: string | number) => void;
71
- }
72
-
73
- /**
74
- * useBottomSheetModal Hook
75
- *
76
- * Hook for managing bottom sheet modal state and interactions.
77
- * Provides imperative methods for controlling the modal.
78
- */
79
4
  export const useBottomSheetModal = (): UseBottomSheetModalReturn => {
80
5
  const modalRef = useRef<BottomSheetModalRef>(null);
81
6
 
82
- /**
83
- * Present bottom sheet modal
84
- */
85
- const present = useCallback(() => {
86
- modalRef.current?.present();
87
- }, []);
88
-
89
- /**
90
- * Dismiss bottom sheet modal
91
- */
92
- const dismiss = useCallback(() => {
93
- modalRef.current?.dismiss();
94
- }, []);
95
-
96
- /**
97
- * Expand to maximum height (last snap point)
98
- */
99
- const expand = useCallback(() => {
100
- modalRef.current?.expand();
101
- }, []);
102
-
103
- /**
104
- * Collapse to minimum height (first snap point)
105
- */
106
- const collapse = useCallback(() => {
107
- modalRef.current?.collapse();
108
- }, []);
7
+ const present = useCallback(() => modalRef.current?.present(), []);
8
+ const dismiss = useCallback(() => modalRef.current?.dismiss(), []);
9
+ const expand = useCallback(() => modalRef.current?.expand(), []);
10
+ const collapse = useCallback(() => modalRef.current?.collapse(), []);
11
+ const snapToIndex = useCallback((index: number) => modalRef.current?.snapToIndex(index), []);
12
+ const snapToPosition = useCallback((pos: string | number) => modalRef.current?.snapToPosition(pos), []);
109
13
 
110
- /**
111
- * Snap to specific index
112
- */
113
- const snapToIndex = useCallback((index: number) => {
114
- modalRef.current?.snapToIndex(index);
115
- }, []);
116
-
117
- /**
118
- * Snap to specific position (percentage or fixed)
119
- */
120
- const snapToPosition = useCallback((position: string | number) => {
121
- modalRef.current?.snapToPosition(position);
122
- }, []);
123
-
124
- return {
125
- modalRef,
126
- present,
127
- dismiss,
128
- expand,
129
- collapse,
130
- snapToIndex,
131
- snapToPosition,
132
- };
14
+ return { modalRef, present, dismiss, expand, collapse, snapToIndex, snapToPosition };
133
15
  };
134
16
 
17
+