@umituz/react-native-design-system 2.3.9 → 2.3.11
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/package.json +1 -1
- package/src/index.ts +45 -9
- package/src/{organisms → layouts/AppHeader}/AppHeader.tsx +2 -2
- package/src/layouts/AppHeader/index.ts +1 -0
- package/src/{molecules → layouts/ScreenHeader}/ScreenHeader.tsx +2 -2
- package/src/layouts/ScreenHeader/index.ts +1 -0
- package/src/{organisms → layouts/ScreenLayout}/ScreenLayout.tsx +4 -4
- package/src/layouts/ScreenLayout/index.ts +1 -0
- package/src/layouts/index.ts +6 -0
- package/src/molecules/bottom-sheet/components/BottomSheet.tsx +122 -0
- package/src/molecules/bottom-sheet/components/BottomSheetModal.tsx +124 -0
- package/src/molecules/bottom-sheet/components/SafeBottomSheetModalProvider.tsx +11 -0
- package/src/molecules/bottom-sheet/components/filter/FilterBottomSheet.tsx +168 -0
- package/src/molecules/bottom-sheet/components/filter/FilterSheet.tsx +116 -0
- package/src/molecules/bottom-sheet/components/filter/FilterSheetComponents/FilterSheetHeader.tsx +33 -0
- package/src/molecules/bottom-sheet/components/filter/FilterSheetComponents/FilterSheetOption.tsx +59 -0
- package/src/molecules/bottom-sheet/hooks/useBottomSheet.ts +16 -0
- package/src/molecules/bottom-sheet/hooks/useBottomSheetModal.ts +17 -0
- package/src/molecules/bottom-sheet/hooks/useListFilters.ts +95 -0
- package/src/molecules/bottom-sheet/index.ts +10 -0
- package/src/molecules/bottom-sheet/types/BottomSheet.ts +122 -0
- package/src/molecules/bottom-sheet/types/Filter.ts +48 -0
- package/src/molecules/index.ts +1 -3
- package/src/organisms/index.ts +1 -13
- /package/src/{molecules → layouts}/Container/Container.tsx +0 -0
- /package/src/{molecules → layouts}/Container/index.ts +0 -0
- /package/src/{organisms → layouts}/FormLayout/FormLayout.tsx +0 -0
- /package/src/{organisms → layouts}/FormLayout/index.ts +0 -0
- /package/src/{molecules → layouts}/Grid/Grid.tsx +0 -0
- /package/src/{molecules → layouts}/Grid/index.ts +0 -0
- /package/src/{organisms → layouts/ScreenLayout}/ScreenLayout.example.tsx +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-design-system",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.11",
|
|
4
4
|
"description": "Universal design system for React Native apps - Consolidated package with atoms, molecules, organisms, theme, typography, responsive and safe area utilities",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -226,6 +226,25 @@ export {
|
|
|
226
226
|
type BadgeSize,
|
|
227
227
|
} from './atoms';
|
|
228
228
|
|
|
229
|
+
// =============================================================================
|
|
230
|
+
// LAYOUTS EXPORTS
|
|
231
|
+
// =============================================================================
|
|
232
|
+
|
|
233
|
+
export {
|
|
234
|
+
ScreenLayout,
|
|
235
|
+
AppHeader,
|
|
236
|
+
ScreenHeader,
|
|
237
|
+
Grid,
|
|
238
|
+
Container,
|
|
239
|
+
FormLayout,
|
|
240
|
+
type ScreenLayoutProps,
|
|
241
|
+
type AppHeaderProps,
|
|
242
|
+
type ScreenHeaderProps,
|
|
243
|
+
type GridProps,
|
|
244
|
+
type ContainerProps,
|
|
245
|
+
type FormLayoutProps,
|
|
246
|
+
} from './layouts';
|
|
247
|
+
|
|
229
248
|
// =============================================================================
|
|
230
249
|
// MOLECULES EXPORTS
|
|
231
250
|
// =============================================================================
|
|
@@ -235,14 +254,36 @@ export {
|
|
|
235
254
|
ListItem,
|
|
236
255
|
SearchBar,
|
|
237
256
|
IconContainer,
|
|
238
|
-
ScreenHeader,
|
|
239
257
|
BaseModal,
|
|
240
258
|
ConfirmationModal,
|
|
241
259
|
useConfirmationModal,
|
|
242
260
|
StepProgress,
|
|
243
|
-
Grid,
|
|
244
261
|
List,
|
|
245
|
-
|
|
262
|
+
Avatar,
|
|
263
|
+
AvatarGroup,
|
|
264
|
+
AvatarUtils,
|
|
265
|
+
type AvatarProps,
|
|
266
|
+
type AvatarGroupProps,
|
|
267
|
+
type AvatarGroupItem,
|
|
268
|
+
type AvatarSize,
|
|
269
|
+
type AvatarShape,
|
|
270
|
+
type AvatarConfig,
|
|
271
|
+
type AvatarType,
|
|
272
|
+
// Bottom Sheet
|
|
273
|
+
BottomSheet,
|
|
274
|
+
BottomSheetModal,
|
|
275
|
+
SafeBottomSheetModalProvider,
|
|
276
|
+
FilterBottomSheet,
|
|
277
|
+
FilterSheet,
|
|
278
|
+
useBottomSheet,
|
|
279
|
+
useBottomSheetModal,
|
|
280
|
+
useListFilters,
|
|
281
|
+
type BottomSheetProps,
|
|
282
|
+
type BottomSheetModalProps,
|
|
283
|
+
type BottomSheetRef,
|
|
284
|
+
type BottomSheetModalRef,
|
|
285
|
+
type FilterOption,
|
|
286
|
+
type FilterCategory,
|
|
246
287
|
// Alerts
|
|
247
288
|
AlertBanner,
|
|
248
289
|
AlertToast,
|
|
@@ -256,9 +297,7 @@ export {
|
|
|
256
297
|
AlertMode,
|
|
257
298
|
AlertPosition,
|
|
258
299
|
type BaseModalProps,
|
|
259
|
-
type GridProps,
|
|
260
300
|
type ListProps,
|
|
261
|
-
type ContainerProps,
|
|
262
301
|
type Alert,
|
|
263
302
|
type AlertAction,
|
|
264
303
|
type AlertOptions,
|
|
@@ -269,11 +308,8 @@ export {
|
|
|
269
308
|
// =============================================================================
|
|
270
309
|
|
|
271
310
|
export {
|
|
272
|
-
ScreenLayout,
|
|
273
|
-
AppHeader,
|
|
274
311
|
FormContainer,
|
|
275
|
-
|
|
276
|
-
type FormLayoutProps,
|
|
312
|
+
type FormContainerProps,
|
|
277
313
|
} from './organisms';
|
|
278
314
|
|
|
279
315
|
// =============================================================================
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
import React from 'react';
|
|
12
12
|
import { View, type ViewStyle } from 'react-native';
|
|
13
13
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
14
|
-
import { useAppDesignTokens } from '
|
|
15
|
-
import { AtomicText, AtomicButton, type IconName } from '
|
|
14
|
+
import { useAppDesignTokens } from '../../theme';
|
|
15
|
+
import { AtomicText, AtomicButton, type IconName } from '../../atoms';
|
|
16
16
|
|
|
17
17
|
// =============================================================================
|
|
18
18
|
// TYPE DEFINITIONS
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './AppHeader';
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
import React from 'react';
|
|
16
16
|
import { View, TouchableOpacity, ViewStyle } from 'react-native';
|
|
17
|
-
import { AtomicIcon, AtomicText } from '
|
|
18
|
-
import { useAppDesignTokens } from '
|
|
17
|
+
import { AtomicIcon, AtomicText } from '../../atoms';
|
|
18
|
+
import { useAppDesignTokens } from '../../theme';
|
|
19
19
|
|
|
20
20
|
export interface ScreenHeaderProps {
|
|
21
21
|
/** Screen title (centered) */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ScreenHeader';
|
|
@@ -27,10 +27,10 @@ import React, { useMemo } from 'react';
|
|
|
27
27
|
import { View, ScrollView, StyleSheet, type ViewStyle, RefreshControlProps } from 'react-native';
|
|
28
28
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
29
29
|
import type { Edge } from 'react-native-safe-area-context';
|
|
30
|
-
import { useAppDesignTokens } from '
|
|
31
|
-
import { useResponsive } from '
|
|
32
|
-
import { getResponsiveMaxWidth } from '
|
|
33
|
-
import { getResponsiveHorizontalPadding } from '
|
|
30
|
+
import { useAppDesignTokens } from '../../theme';
|
|
31
|
+
import { useResponsive } from '../../responsive/useResponsive';
|
|
32
|
+
import { getResponsiveMaxWidth } from '../../responsive/responsiveSizing';
|
|
33
|
+
import { getResponsiveHorizontalPadding } from '../../responsive/responsiveLayout';
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* NOTE: This component now works in conjunction with the SafeAreaProvider
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ScreenLayout';
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback, useMemo, useImperativeHandle, useRef } from 'react';
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
3
|
+
import GorhomBottomSheet, {
|
|
4
|
+
BottomSheetView,
|
|
5
|
+
BottomSheetBackdrop,
|
|
6
|
+
type BottomSheetBackdropProps,
|
|
7
|
+
} from '@gorhom/bottom-sheet';
|
|
8
|
+
import { useAppDesignTokens } from '../../../theme';
|
|
9
|
+
import type {
|
|
10
|
+
BottomSheetConfig,
|
|
11
|
+
BottomSheetRef,
|
|
12
|
+
BottomSheetProps,
|
|
13
|
+
} from '../types/BottomSheet';
|
|
14
|
+
import { BottomSheetUtils } from '../types/BottomSheet';
|
|
15
|
+
|
|
16
|
+
export const BottomSheet = forwardRef<BottomSheetRef, BottomSheetProps>((props, ref) => {
|
|
17
|
+
const {
|
|
18
|
+
children,
|
|
19
|
+
preset = 'medium',
|
|
20
|
+
snapPoints: customSnapPoints,
|
|
21
|
+
initialIndex,
|
|
22
|
+
enableBackdrop = true,
|
|
23
|
+
backdropAppearsOnIndex,
|
|
24
|
+
backdropDisappearsOnIndex,
|
|
25
|
+
keyboardBehavior = 'interactive',
|
|
26
|
+
enableHandleIndicator = true,
|
|
27
|
+
enablePanDownToClose = true,
|
|
28
|
+
enableDynamicSizing = false,
|
|
29
|
+
onChange,
|
|
30
|
+
onClose,
|
|
31
|
+
} = props;
|
|
32
|
+
|
|
33
|
+
const tokens = useAppDesignTokens();
|
|
34
|
+
const sheetRef = useRef<GorhomBottomSheet>(null);
|
|
35
|
+
|
|
36
|
+
const config: BottomSheetConfig = useMemo(() => {
|
|
37
|
+
if (customSnapPoints) {
|
|
38
|
+
return BottomSheetUtils.createConfig({
|
|
39
|
+
snapPoints: customSnapPoints,
|
|
40
|
+
initialIndex,
|
|
41
|
+
enableBackdrop,
|
|
42
|
+
backdropAppearsOnIndex,
|
|
43
|
+
backdropDisappearsOnIndex,
|
|
44
|
+
keyboardBehavior,
|
|
45
|
+
enableHandleIndicator,
|
|
46
|
+
enablePanDownToClose,
|
|
47
|
+
enableDynamicSizing,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return BottomSheetUtils.getPreset(preset);
|
|
51
|
+
}, [preset, customSnapPoints, initialIndex, enableBackdrop, backdropAppearsOnIndex, backdropDisappearsOnIndex, keyboardBehavior, enableHandleIndicator, enablePanDownToClose, enableDynamicSizing]);
|
|
52
|
+
|
|
53
|
+
const renderBackdrop = useCallback(
|
|
54
|
+
(backdropProps: BottomSheetBackdropProps) =>
|
|
55
|
+
enableBackdrop ? (
|
|
56
|
+
<BottomSheetBackdrop
|
|
57
|
+
{...backdropProps}
|
|
58
|
+
appearsOnIndex={config.backdropAppearsOnIndex ?? 0}
|
|
59
|
+
disappearsOnIndex={config.backdropDisappearsOnIndex ?? -1}
|
|
60
|
+
opacity={0.5}
|
|
61
|
+
pressBehavior="close"
|
|
62
|
+
/>
|
|
63
|
+
) : null,
|
|
64
|
+
[enableBackdrop, config.backdropAppearsOnIndex, config.backdropDisappearsOnIndex]
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const handleSheetChange = useCallback(
|
|
68
|
+
(index: number) => {
|
|
69
|
+
onChange?.(index);
|
|
70
|
+
if (index === -1) onClose?.();
|
|
71
|
+
},
|
|
72
|
+
[onChange, onClose]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
useImperativeHandle(ref, () => ({
|
|
76
|
+
snapToIndex: (index: number) => sheetRef.current?.snapToIndex(index),
|
|
77
|
+
snapToPosition: (pos: string | number) => sheetRef.current?.snapToPosition(pos),
|
|
78
|
+
expand: () => sheetRef.current?.expand(),
|
|
79
|
+
collapse: () => sheetRef.current?.collapse(),
|
|
80
|
+
close: () => sheetRef.current?.close(),
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
if (!config.snapPoints || config.snapPoints.length === 0) return null;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<GorhomBottomSheet
|
|
87
|
+
ref={sheetRef}
|
|
88
|
+
index={-1}
|
|
89
|
+
snapPoints={config.snapPoints}
|
|
90
|
+
enableDynamicSizing={config.enableDynamicSizing}
|
|
91
|
+
backdropComponent={renderBackdrop}
|
|
92
|
+
keyboardBehavior={config.keyboardBehavior}
|
|
93
|
+
enableHandlePanningGesture={config.enableHandleIndicator}
|
|
94
|
+
enablePanDownToClose={config.enablePanDownToClose}
|
|
95
|
+
onChange={handleSheetChange}
|
|
96
|
+
backgroundStyle={[styles.background, { backgroundColor: tokens.colors.surface }]}
|
|
97
|
+
handleIndicatorStyle={[styles.handleIndicator, { backgroundColor: tokens.colors.border }]}
|
|
98
|
+
>
|
|
99
|
+
<BottomSheetView style={styles.contentContainer}>
|
|
100
|
+
{children}
|
|
101
|
+
</BottomSheetView>
|
|
102
|
+
</GorhomBottomSheet>
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
BottomSheet.displayName = 'BottomSheet';
|
|
107
|
+
|
|
108
|
+
const styles = StyleSheet.create({
|
|
109
|
+
background: {
|
|
110
|
+
borderTopLeftRadius: 16,
|
|
111
|
+
borderTopRightRadius: 16,
|
|
112
|
+
},
|
|
113
|
+
handleIndicator: {
|
|
114
|
+
width: 40,
|
|
115
|
+
height: 4,
|
|
116
|
+
borderRadius: 2,
|
|
117
|
+
},
|
|
118
|
+
contentContainer: {
|
|
119
|
+
flex: 1,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback, useMemo, useImperativeHandle, useRef } from 'react';
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
BottomSheetModal as GorhomBottomSheetModal,
|
|
5
|
+
BottomSheetView,
|
|
6
|
+
BottomSheetBackdrop,
|
|
7
|
+
type BottomSheetBackdropProps,
|
|
8
|
+
} from '@gorhom/bottom-sheet';
|
|
9
|
+
import { useAppDesignTokens } from '../../../theme';
|
|
10
|
+
import type {
|
|
11
|
+
BottomSheetConfig,
|
|
12
|
+
BottomSheetModalRef,
|
|
13
|
+
BottomSheetModalProps,
|
|
14
|
+
} from '../types/BottomSheet';
|
|
15
|
+
import { BottomSheetUtils } from '../types/BottomSheet';
|
|
16
|
+
|
|
17
|
+
export const BottomSheetModal = forwardRef<BottomSheetModalRef, BottomSheetModalProps>((props, ref) => {
|
|
18
|
+
const {
|
|
19
|
+
children,
|
|
20
|
+
preset = 'medium',
|
|
21
|
+
snapPoints: customSnapPoints,
|
|
22
|
+
initialIndex,
|
|
23
|
+
enableBackdrop = true,
|
|
24
|
+
backdropAppearsOnIndex,
|
|
25
|
+
backdropDisappearsOnIndex,
|
|
26
|
+
keyboardBehavior = 'interactive',
|
|
27
|
+
enableHandleIndicator = true,
|
|
28
|
+
enablePanDownToClose = true,
|
|
29
|
+
enableDynamicSizing = false,
|
|
30
|
+
onChange,
|
|
31
|
+
onDismiss,
|
|
32
|
+
backgroundColor,
|
|
33
|
+
} = props;
|
|
34
|
+
|
|
35
|
+
const tokens = useAppDesignTokens();
|
|
36
|
+
const modalRef = useRef<GorhomBottomSheetModal>(null);
|
|
37
|
+
|
|
38
|
+
const config: BottomSheetConfig = useMemo(() => {
|
|
39
|
+
if (customSnapPoints) {
|
|
40
|
+
return BottomSheetUtils.createConfig({
|
|
41
|
+
snapPoints: customSnapPoints,
|
|
42
|
+
initialIndex,
|
|
43
|
+
enableBackdrop,
|
|
44
|
+
backdropAppearsOnIndex,
|
|
45
|
+
backdropDisappearsOnIndex,
|
|
46
|
+
keyboardBehavior,
|
|
47
|
+
enableHandleIndicator,
|
|
48
|
+
enablePanDownToClose,
|
|
49
|
+
enableDynamicSizing,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return BottomSheetUtils.getPreset(preset);
|
|
53
|
+
}, [preset, customSnapPoints, initialIndex, enableBackdrop, backdropAppearsOnIndex, backdropDisappearsOnIndex, keyboardBehavior, enableHandleIndicator, enablePanDownToClose, enableDynamicSizing]);
|
|
54
|
+
|
|
55
|
+
const renderBackdrop = useCallback(
|
|
56
|
+
(backdropProps: BottomSheetBackdropProps) =>
|
|
57
|
+
enableBackdrop ? (
|
|
58
|
+
<BottomSheetBackdrop
|
|
59
|
+
{...backdropProps}
|
|
60
|
+
appearsOnIndex={config.backdropAppearsOnIndex ?? 0}
|
|
61
|
+
disappearsOnIndex={config.backdropDisappearsOnIndex ?? -1}
|
|
62
|
+
opacity={0.5}
|
|
63
|
+
pressBehavior="close"
|
|
64
|
+
/>
|
|
65
|
+
) : null,
|
|
66
|
+
[enableBackdrop, config.backdropAppearsOnIndex, config.backdropDisappearsOnIndex]
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const handleSheetChange = useCallback(
|
|
70
|
+
(index: number) => {
|
|
71
|
+
onChange?.(index);
|
|
72
|
+
if (index === -1) onDismiss?.();
|
|
73
|
+
},
|
|
74
|
+
[onChange, onDismiss]
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
useImperativeHandle(ref, () => ({
|
|
78
|
+
present: () => modalRef.current?.present(),
|
|
79
|
+
dismiss: () => modalRef.current?.dismiss(),
|
|
80
|
+
snapToIndex: (index: number) => modalRef.current?.snapToIndex(index),
|
|
81
|
+
snapToPosition: (pos: string | number) => modalRef.current?.snapToPosition(pos),
|
|
82
|
+
expand: () => modalRef.current?.expand(),
|
|
83
|
+
collapse: () => modalRef.current?.collapse(),
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<GorhomBottomSheetModal
|
|
88
|
+
ref={modalRef}
|
|
89
|
+
index={-1}
|
|
90
|
+
snapPoints={config.snapPoints}
|
|
91
|
+
enableDynamicSizing={config.enableDynamicSizing}
|
|
92
|
+
backdropComponent={renderBackdrop}
|
|
93
|
+
keyboardBehavior={config.keyboardBehavior}
|
|
94
|
+
enableHandlePanningGesture={config.enableHandleIndicator}
|
|
95
|
+
enablePanDownToClose={config.enablePanDownToClose}
|
|
96
|
+
onChange={handleSheetChange}
|
|
97
|
+
onDismiss={onDismiss}
|
|
98
|
+
backgroundStyle={[styles.background, { backgroundColor: backgroundColor || tokens.colors.surface }]}
|
|
99
|
+
handleIndicatorStyle={[styles.handleIndicator, { backgroundColor: tokens.colors.border }]}
|
|
100
|
+
>
|
|
101
|
+
<BottomSheetView style={styles.contentContainer}>
|
|
102
|
+
{children}
|
|
103
|
+
</BottomSheetView>
|
|
104
|
+
</GorhomBottomSheetModal>
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
BottomSheetModal.displayName = 'BottomSheetModal';
|
|
109
|
+
|
|
110
|
+
const styles = StyleSheet.create({
|
|
111
|
+
background: {
|
|
112
|
+
borderTopLeftRadius: 16,
|
|
113
|
+
borderTopRightRadius: 16,
|
|
114
|
+
},
|
|
115
|
+
handleIndicator: {
|
|
116
|
+
width: 40,
|
|
117
|
+
height: 4,
|
|
118
|
+
borderRadius: 2,
|
|
119
|
+
},
|
|
120
|
+
contentContainer: {
|
|
121
|
+
flex: 1,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
|
|
3
|
+
|
|
4
|
+
interface SafeBottomSheetModalProviderProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const SafeBottomSheetModalProvider = ({ children }: SafeBottomSheetModalProviderProps) => (
|
|
9
|
+
<BottomSheetModalProvider>{children}</BottomSheetModalProvider>
|
|
10
|
+
);
|
|
11
|
+
|
|
@@ -0,0 +1,168 @@
|
|
|
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 '../../types/BottomSheet';
|
|
11
|
+
import { AtomicText, AtomicIcon, AtomicButton } from "../../../../atoms";
|
|
12
|
+
import { useAppDesignTokens } from '../../../../theme';
|
|
13
|
+
import type { FilterOption, FilterCategory } from '../../types/Filter';
|
|
14
|
+
import { FilterUtils } from '../../types/Filter';
|
|
15
|
+
|
|
16
|
+
export interface FilterBottomSheetProps {
|
|
17
|
+
readonly categories: FilterCategory[];
|
|
18
|
+
readonly selectedIds: string[];
|
|
19
|
+
readonly onFilterPress: (id: string, categoryId: string) => void;
|
|
20
|
+
readonly onClearFilters: () => void;
|
|
21
|
+
readonly onDismiss?: () => void;
|
|
22
|
+
readonly title?: string;
|
|
23
|
+
readonly clearLabel?: string;
|
|
24
|
+
readonly applyLabel?: string;
|
|
25
|
+
readonly defaultId?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const FilterBottomSheet = forwardRef<BottomSheetModalRef, FilterBottomSheetProps>(({
|
|
29
|
+
categories,
|
|
30
|
+
selectedIds,
|
|
31
|
+
onFilterPress,
|
|
32
|
+
onClearFilters,
|
|
33
|
+
onDismiss,
|
|
34
|
+
title,
|
|
35
|
+
clearLabel = 'Clear',
|
|
36
|
+
applyLabel = 'Apply',
|
|
37
|
+
defaultId = 'all'
|
|
38
|
+
}, ref) => {
|
|
39
|
+
const tokens = useAppDesignTokens();
|
|
40
|
+
|
|
41
|
+
const styles = React.useMemo(() => StyleSheet.create({
|
|
42
|
+
container: {
|
|
43
|
+
flex: 1,
|
|
44
|
+
padding: 16,
|
|
45
|
+
},
|
|
46
|
+
header: {
|
|
47
|
+
flexDirection: 'row',
|
|
48
|
+
justifyContent: 'space-between',
|
|
49
|
+
alignItems: 'center',
|
|
50
|
+
marginBottom: 20,
|
|
51
|
+
},
|
|
52
|
+
category: {
|
|
53
|
+
marginBottom: 24,
|
|
54
|
+
},
|
|
55
|
+
categoryTitle: {
|
|
56
|
+
marginBottom: 12,
|
|
57
|
+
opacity: 0.7,
|
|
58
|
+
},
|
|
59
|
+
optionsGrid: {
|
|
60
|
+
flexDirection: 'row',
|
|
61
|
+
flexWrap: 'wrap',
|
|
62
|
+
gap: 8,
|
|
63
|
+
},
|
|
64
|
+
option: {
|
|
65
|
+
flexDirection: 'row',
|
|
66
|
+
alignItems: 'center',
|
|
67
|
+
paddingHorizontal: 12,
|
|
68
|
+
paddingVertical: 8,
|
|
69
|
+
borderRadius: 20,
|
|
70
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
71
|
+
gap: 6,
|
|
72
|
+
borderWidth: 1,
|
|
73
|
+
borderColor: 'transparent',
|
|
74
|
+
},
|
|
75
|
+
optionLeft: {
|
|
76
|
+
flexDirection: 'row',
|
|
77
|
+
alignItems: 'center',
|
|
78
|
+
gap: 6,
|
|
79
|
+
},
|
|
80
|
+
footer: {
|
|
81
|
+
marginTop: 16,
|
|
82
|
+
paddingBottom: 8,
|
|
83
|
+
}
|
|
84
|
+
}), [tokens]);
|
|
85
|
+
|
|
86
|
+
const renderOption = useCallback((option: FilterOption, categoryId: string) => {
|
|
87
|
+
const isSelected = selectedIds.includes(option.id);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<TouchableOpacity
|
|
91
|
+
key={option.id}
|
|
92
|
+
style={[
|
|
93
|
+
styles.option,
|
|
94
|
+
isSelected && { backgroundColor: tokens.colors.primary + '15' }
|
|
95
|
+
]}
|
|
96
|
+
onPress={() => onFilterPress(option.id, categoryId)}
|
|
97
|
+
>
|
|
98
|
+
<View style={styles.optionLeft}>
|
|
99
|
+
{option.icon && (
|
|
100
|
+
<AtomicIcon
|
|
101
|
+
name={option.icon as any}
|
|
102
|
+
size="sm"
|
|
103
|
+
color={isSelected ? 'primary' : 'secondary'}
|
|
104
|
+
/>
|
|
105
|
+
)}
|
|
106
|
+
<AtomicText
|
|
107
|
+
type="bodyMedium"
|
|
108
|
+
style={[isSelected && { color: tokens.colors.primary, fontWeight: '600' }]}
|
|
109
|
+
>
|
|
110
|
+
{option.label}
|
|
111
|
+
</AtomicText>
|
|
112
|
+
</View>
|
|
113
|
+
{isSelected && (
|
|
114
|
+
<AtomicIcon name="checkmark" size="sm" color="primary" />
|
|
115
|
+
)}
|
|
116
|
+
</TouchableOpacity>
|
|
117
|
+
);
|
|
118
|
+
}, [selectedIds, tokens, onFilterPress]);
|
|
119
|
+
|
|
120
|
+
const renderCategory = useCallback((category: FilterCategory) => (
|
|
121
|
+
<View key={category.id} style={styles.category}>
|
|
122
|
+
<AtomicText type="labelLarge" style={styles.categoryTitle}>
|
|
123
|
+
{category.title}
|
|
124
|
+
</AtomicText>
|
|
125
|
+
<View style={styles.optionsGrid}>
|
|
126
|
+
{category.options.map(option => renderOption(option, category.id))}
|
|
127
|
+
</View>
|
|
128
|
+
</View>
|
|
129
|
+
), [renderOption, styles]);
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
const hasActiveFilters = FilterUtils.hasActiveFilter(selectedIds, defaultId);
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<BottomSheetModal
|
|
136
|
+
ref={ref}
|
|
137
|
+
preset="medium"
|
|
138
|
+
onDismiss={onDismiss}
|
|
139
|
+
backgroundColor={tokens.colors.surface}
|
|
140
|
+
>
|
|
141
|
+
<View style={styles.container}>
|
|
142
|
+
<View style={styles.header}>
|
|
143
|
+
<AtomicText type="headlineSmall">{title || 'Filter'}</AtomicText>
|
|
144
|
+
{hasActiveFilters && (
|
|
145
|
+
<TouchableOpacity onPress={onClearFilters}>
|
|
146
|
+
<AtomicText type="labelLarge" color="error">{clearLabel}</AtomicText>
|
|
147
|
+
</TouchableOpacity>
|
|
148
|
+
)}
|
|
149
|
+
</View>
|
|
150
|
+
|
|
151
|
+
<ScrollView showsVerticalScrollIndicator={false}>
|
|
152
|
+
{categories.map(renderCategory)}
|
|
153
|
+
</ScrollView>
|
|
154
|
+
|
|
155
|
+
<View style={styles.footer}>
|
|
156
|
+
<AtomicButton
|
|
157
|
+
onPress={() => (ref as any).current?.dismiss()}
|
|
158
|
+
fullWidth
|
|
159
|
+
>
|
|
160
|
+
{applyLabel}
|
|
161
|
+
</AtomicButton>
|
|
162
|
+
</View>
|
|
163
|
+
</View>
|
|
164
|
+
</BottomSheetModal>
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
FilterBottomSheet.displayName = 'FilterBottomSheet';
|
|
@@ -0,0 +1,116 @@
|
|
|
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 { AtomicText, AtomicIcon, AtomicButton } from "../../../../atoms";
|
|
5
|
+
import { useAppDesignTokens } from "../../../../theme";
|
|
6
|
+
import type { FilterOption, FilterCategory } from "../../types/Filter";
|
|
7
|
+
import { FilterUtils } from "../../types/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
|
+
clearLabel?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const FilterSheet: React.FC<FilterSheetProps> = ({
|
|
24
|
+
visible,
|
|
25
|
+
options,
|
|
26
|
+
selectedIds,
|
|
27
|
+
onFilterPress,
|
|
28
|
+
onClearFilters,
|
|
29
|
+
onClose,
|
|
30
|
+
defaultFilterId = "all",
|
|
31
|
+
title,
|
|
32
|
+
clearLabel = "Clear"
|
|
33
|
+
}) => {
|
|
34
|
+
const tokens = useAppDesignTokens();
|
|
35
|
+
const insets = useSafeAreaInsets();
|
|
36
|
+
|
|
37
|
+
const hasActiveFilter = FilterUtils.hasActiveFilter(selectedIds, defaultFilterId);
|
|
38
|
+
|
|
39
|
+
const handleFilterPressWithClose = useCallback((id: string) => {
|
|
40
|
+
onFilterPress(id);
|
|
41
|
+
onClose?.();
|
|
42
|
+
}, [onFilterPress, onClose]);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
|
|
46
|
+
<Pressable style={styles.backdrop} onPress={onClose}>
|
|
47
|
+
<Pressable
|
|
48
|
+
style={[styles.sheet, { backgroundColor: tokens.colors.surface, paddingBottom: insets.bottom }]}
|
|
49
|
+
onPress={(e) => e.stopPropagation()}
|
|
50
|
+
>
|
|
51
|
+
<View style={[styles.handle, { backgroundColor: tokens.colors.border }]} />
|
|
52
|
+
|
|
53
|
+
<FilterSheetHeader
|
|
54
|
+
title={title || "Filter"}
|
|
55
|
+
onClose={() => onClose?.()}
|
|
56
|
+
tokens={tokens}
|
|
57
|
+
/>
|
|
58
|
+
|
|
59
|
+
<ScrollView style={styles.optionsList} showsVerticalScrollIndicator={false}>
|
|
60
|
+
{options.map((option) => (
|
|
61
|
+
<FilterSheetOption
|
|
62
|
+
key={option.id}
|
|
63
|
+
option={option}
|
|
64
|
+
isSelected={selectedIds.includes(option.id)}
|
|
65
|
+
onPress={handleFilterPressWithClose}
|
|
66
|
+
tokens={tokens}
|
|
67
|
+
/>
|
|
68
|
+
))}
|
|
69
|
+
</ScrollView>
|
|
70
|
+
|
|
71
|
+
{hasActiveFilter && (
|
|
72
|
+
<View style={[styles.footer, { borderTopColor: tokens.colors.border, borderTopWidth: tokens.borders.width.thin }]}>
|
|
73
|
+
<AtomicButton variant="outline" onPress={onClearFilters} fullWidth>
|
|
74
|
+
{clearLabel}
|
|
75
|
+
</AtomicButton>
|
|
76
|
+
</View>
|
|
77
|
+
)}
|
|
78
|
+
</Pressable>
|
|
79
|
+
</Pressable>
|
|
80
|
+
</Modal>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
FilterSheet.displayName = "FilterSheet";
|
|
85
|
+
|
|
86
|
+
const styles = StyleSheet.create({
|
|
87
|
+
backdrop: {
|
|
88
|
+
flex: 1,
|
|
89
|
+
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
90
|
+
justifyContent: "flex-end",
|
|
91
|
+
},
|
|
92
|
+
sheet: {
|
|
93
|
+
borderTopLeftRadius: 16,
|
|
94
|
+
borderTopRightRadius: 16,
|
|
95
|
+
maxHeight: "80%",
|
|
96
|
+
},
|
|
97
|
+
handle: {
|
|
98
|
+
width: 40,
|
|
99
|
+
height: 4,
|
|
100
|
+
borderRadius: 2,
|
|
101
|
+
alignSelf: "center",
|
|
102
|
+
marginTop: 8,
|
|
103
|
+
marginBottom: 8,
|
|
104
|
+
},
|
|
105
|
+
optionsList: {
|
|
106
|
+
maxHeight: 400,
|
|
107
|
+
paddingVertical: 8,
|
|
108
|
+
},
|
|
109
|
+
footer: {
|
|
110
|
+
paddingHorizontal: 20,
|
|
111
|
+
paddingTop: 16,
|
|
112
|
+
paddingBottom: 8,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
|
package/src/molecules/bottom-sheet/components/filter/FilterSheetComponents/FilterSheetHeader.tsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
|
3
|
+
import { AtomicText, AtomicIcon } from '../../../../../atoms';
|
|
4
|
+
import type { useAppDesignTokens } from '../../../../../theme';
|
|
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
|
+
});
|
package/src/molecules/bottom-sheet/components/filter/FilterSheetComponents/FilterSheetOption.tsx
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet, TouchableOpacity } from 'react-native';
|
|
3
|
+
import { AtomicText, AtomicIcon } from '../../../../../atoms';
|
|
4
|
+
import type { useAppDesignTokens } from '../../../../../theme';
|
|
5
|
+
import type { FilterOption } from '../../../types/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
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useRef, useCallback } from 'react';
|
|
2
|
+
import type { BottomSheetRef, UseBottomSheetReturn } from '../types/BottomSheet';
|
|
3
|
+
|
|
4
|
+
export const useBottomSheet = (): UseBottomSheetReturn => {
|
|
5
|
+
const sheetRef = useRef<BottomSheetRef>(null);
|
|
6
|
+
|
|
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), []);
|
|
13
|
+
|
|
14
|
+
return { sheetRef, open, close, expand, collapse, snapToIndex, snapToPosition };
|
|
15
|
+
};
|
|
16
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useRef, useCallback } from 'react';
|
|
2
|
+
import type { BottomSheetModalRef, UseBottomSheetModalReturn } from '../types/BottomSheet';
|
|
3
|
+
|
|
4
|
+
export const useBottomSheetModal = (): UseBottomSheetModalReturn => {
|
|
5
|
+
const modalRef = useRef<BottomSheetModalRef>(null);
|
|
6
|
+
|
|
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), []);
|
|
13
|
+
|
|
14
|
+
return { modalRef, present, dismiss, expand, collapse, snapToIndex, snapToPosition };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useListFilters Hook
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
6
|
+
import { FilterOption } from '../types/Filter';
|
|
7
|
+
|
|
8
|
+
export interface FilterItem extends FilterOption {
|
|
9
|
+
active?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface UseListFiltersReturn<T> {
|
|
13
|
+
filters: FilterItem[];
|
|
14
|
+
setFilters: (filters: FilterItem[]) => void;
|
|
15
|
+
activeFilters: FilterItem[];
|
|
16
|
+
selectedIds: string[];
|
|
17
|
+
applyFilters: (items: T[]) => T[];
|
|
18
|
+
toggleFilter: (id: string) => void;
|
|
19
|
+
handleFilterPress: (id: string) => void;
|
|
20
|
+
clearFilters: () => void;
|
|
21
|
+
handleClearFilters: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UseListFiltersConfig<T> {
|
|
25
|
+
options: FilterOption[];
|
|
26
|
+
defaultFilterId?: string;
|
|
27
|
+
singleSelect?: boolean;
|
|
28
|
+
filterFn?: (item: T, activeFilters: FilterItem[]) => boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Overload 1: When config object is passed
|
|
32
|
+
export function useListFilters<T>(config: UseListFiltersConfig<T>): UseListFiltersReturn<T>;
|
|
33
|
+
// Overload 2: When initial filters array and optional filterFn are passed
|
|
34
|
+
export function useListFilters<T>(initialFilters: FilterItem[], filterFn?: (item: T, activeFilters: FilterItem[]) => boolean): UseListFiltersReturn<T>;
|
|
35
|
+
// Unified implementation
|
|
36
|
+
export function useListFilters<T>(
|
|
37
|
+
configOrFilters: FilterItem[] | UseListFiltersConfig<T>,
|
|
38
|
+
filterFnParam?: (item: T, activeFilters: FilterItem[]) => boolean
|
|
39
|
+
): UseListFiltersReturn<T> {
|
|
40
|
+
const isConfig = !Array.isArray(configOrFilters);
|
|
41
|
+
|
|
42
|
+
// Normalize initial state
|
|
43
|
+
const initialFilters = isConfig
|
|
44
|
+
? (configOrFilters as UseListFiltersConfig<T>).options.map(opt => ({
|
|
45
|
+
...opt,
|
|
46
|
+
active: (configOrFilters as UseListFiltersConfig<T>).defaultFilterId
|
|
47
|
+
? opt.id === (configOrFilters as UseListFiltersConfig<T>).defaultFilterId
|
|
48
|
+
: false
|
|
49
|
+
}))
|
|
50
|
+
: (configOrFilters as FilterItem[]);
|
|
51
|
+
|
|
52
|
+
const filterFn = isConfig
|
|
53
|
+
? (configOrFilters as UseListFiltersConfig<T>).filterFn
|
|
54
|
+
: filterFnParam;
|
|
55
|
+
|
|
56
|
+
const singleSelect = isConfig
|
|
57
|
+
? (configOrFilters as UseListFiltersConfig<T>).singleSelect
|
|
58
|
+
: false;
|
|
59
|
+
|
|
60
|
+
const [filters, setFilters] = useState<FilterItem[]>(initialFilters);
|
|
61
|
+
|
|
62
|
+
const activeFilters = useMemo(() => filters.filter((f) => f.active), [filters]);
|
|
63
|
+
|
|
64
|
+
const toggleFilter = useCallback((id: string) => {
|
|
65
|
+
setFilters((current) => {
|
|
66
|
+
if (singleSelect) {
|
|
67
|
+
return current.map((f) => ({ ...f, active: f.id === id }));
|
|
68
|
+
}
|
|
69
|
+
return current.map((f) => (f.id === id ? { ...f, active: !f.active } : f));
|
|
70
|
+
});
|
|
71
|
+
}, [singleSelect]);
|
|
72
|
+
|
|
73
|
+
const clearFilters = useCallback(() => {
|
|
74
|
+
setFilters((current) => current.map((f) => ({ ...f, active: false })));
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
const applyFilters = useCallback((items: T[]) => {
|
|
78
|
+
if (activeFilters.length === 0 || !filterFn) return items;
|
|
79
|
+
return items.filter((item) => filterFn(item, activeFilters));
|
|
80
|
+
}, [activeFilters, filterFn]);
|
|
81
|
+
|
|
82
|
+
const selectedIds = useMemo(() => activeFilters.map((f) => f.id), [activeFilters]);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
filters,
|
|
86
|
+
setFilters,
|
|
87
|
+
activeFilters,
|
|
88
|
+
selectedIds,
|
|
89
|
+
applyFilters,
|
|
90
|
+
toggleFilter,
|
|
91
|
+
handleFilterPress: toggleFilter,
|
|
92
|
+
clearFilters,
|
|
93
|
+
handleClearFilters: clearFilters,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './components/BottomSheet';
|
|
2
|
+
export * from './components/BottomSheetModal';
|
|
3
|
+
export * from './components/SafeBottomSheetModalProvider';
|
|
4
|
+
export * from './components/filter/FilterBottomSheet';
|
|
5
|
+
export * from './components/filter/FilterSheet';
|
|
6
|
+
export * from './hooks/useBottomSheet';
|
|
7
|
+
export * from './hooks/useBottomSheetModal';
|
|
8
|
+
export * from './hooks/useListFilters';
|
|
9
|
+
export * from './types/BottomSheet';
|
|
10
|
+
export * from './types/Filter';
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export type SnapPoint = string | number;
|
|
4
|
+
export type BottomSheetPreset = 'small' | 'medium' | 'large' | 'full' | 'custom';
|
|
5
|
+
export type KeyboardBehavior = 'interactive' | 'extend' | 'fillParent';
|
|
6
|
+
|
|
7
|
+
export interface BottomSheetConfig {
|
|
8
|
+
snapPoints: SnapPoint[];
|
|
9
|
+
initialIndex?: number;
|
|
10
|
+
enableBackdrop?: boolean;
|
|
11
|
+
backdropAppearsOnIndex?: number;
|
|
12
|
+
backdropDisappearsOnIndex?: number;
|
|
13
|
+
keyboardBehavior?: KeyboardBehavior;
|
|
14
|
+
enableHandleIndicator?: boolean;
|
|
15
|
+
enablePanDownToClose?: boolean;
|
|
16
|
+
enableDynamicSizing?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface BottomSheetRef {
|
|
20
|
+
snapToIndex: (index: number) => void;
|
|
21
|
+
snapToPosition: (position: string | number) => void;
|
|
22
|
+
expand: () => void;
|
|
23
|
+
collapse: () => void;
|
|
24
|
+
close: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BottomSheetModalRef {
|
|
28
|
+
present: () => void;
|
|
29
|
+
dismiss: () => void;
|
|
30
|
+
snapToIndex: (index: number) => void;
|
|
31
|
+
snapToPosition: (position: string | number) => void;
|
|
32
|
+
expand: () => void;
|
|
33
|
+
collapse: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface BottomSheetProps {
|
|
37
|
+
children: React.ReactNode;
|
|
38
|
+
preset?: BottomSheetPreset;
|
|
39
|
+
snapPoints?: SnapPoint[];
|
|
40
|
+
initialIndex?: number;
|
|
41
|
+
enableBackdrop?: boolean;
|
|
42
|
+
backdropAppearsOnIndex?: number;
|
|
43
|
+
backdropDisappearsOnIndex?: number;
|
|
44
|
+
keyboardBehavior?: KeyboardBehavior;
|
|
45
|
+
enableHandleIndicator?: boolean;
|
|
46
|
+
enablePanDownToClose?: boolean;
|
|
47
|
+
enableDynamicSizing?: boolean;
|
|
48
|
+
onChange?: (index: number) => void;
|
|
49
|
+
onClose?: () => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface BottomSheetModalProps extends Omit<BottomSheetProps, 'onClose'> {
|
|
53
|
+
onDismiss?: () => void;
|
|
54
|
+
backgroundColor?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface UseBottomSheetReturn {
|
|
58
|
+
sheetRef: React.RefObject<BottomSheetRef | null>;
|
|
59
|
+
open: () => void;
|
|
60
|
+
close: () => void;
|
|
61
|
+
expand: () => void;
|
|
62
|
+
collapse: () => void;
|
|
63
|
+
snapToIndex: (index: number) => void;
|
|
64
|
+
snapToPosition: (position: string | number) => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface UseBottomSheetModalReturn {
|
|
68
|
+
modalRef: React.RefObject<BottomSheetModalRef | null>;
|
|
69
|
+
present: () => void;
|
|
70
|
+
dismiss: () => void;
|
|
71
|
+
snapToIndex: (index: number) => void;
|
|
72
|
+
snapToPosition: (position: string | number) => void;
|
|
73
|
+
expand: () => void;
|
|
74
|
+
collapse: () => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
export const BottomSheetUtils = {
|
|
79
|
+
getPreset: (preset: BottomSheetPreset): BottomSheetConfig => {
|
|
80
|
+
switch (preset) {
|
|
81
|
+
case 'small':
|
|
82
|
+
return {
|
|
83
|
+
snapPoints: ['25%'],
|
|
84
|
+
enableBackdrop: true,
|
|
85
|
+
enablePanDownToClose: true,
|
|
86
|
+
};
|
|
87
|
+
case 'medium':
|
|
88
|
+
return {
|
|
89
|
+
snapPoints: ['50%'],
|
|
90
|
+
enableBackdrop: true,
|
|
91
|
+
enablePanDownToClose: true,
|
|
92
|
+
};
|
|
93
|
+
case 'large':
|
|
94
|
+
return {
|
|
95
|
+
snapPoints: ['90%'],
|
|
96
|
+
enableBackdrop: true,
|
|
97
|
+
enablePanDownToClose: true,
|
|
98
|
+
};
|
|
99
|
+
case 'full':
|
|
100
|
+
return {
|
|
101
|
+
snapPoints: ['100%'],
|
|
102
|
+
enableBackdrop: true,
|
|
103
|
+
enablePanDownToClose: false,
|
|
104
|
+
};
|
|
105
|
+
default:
|
|
106
|
+
return {
|
|
107
|
+
snapPoints: ['50%'],
|
|
108
|
+
enableBackdrop: true,
|
|
109
|
+
enablePanDownToClose: true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
createConfig: (config: Partial<BottomSheetConfig>): BottomSheetConfig => {
|
|
115
|
+
return {
|
|
116
|
+
snapPoints: ['50%'],
|
|
117
|
+
enableBackdrop: true,
|
|
118
|
+
enablePanDownToClose: true,
|
|
119
|
+
...config,
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain - Filter Entities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface FilterOption {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
icon?: string;
|
|
9
|
+
type?: string;
|
|
10
|
+
count?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FilterCategory {
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
options: FilterOption[];
|
|
17
|
+
multiSelect?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class FilterUtils {
|
|
21
|
+
static hasActiveFilter(selectedIds: string[], defaultId: string = "all"): boolean {
|
|
22
|
+
if (selectedIds.length === 0) return false;
|
|
23
|
+
if (selectedIds.length === 1 && selectedIds[0] === defaultId) return false;
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static toggleFilter(
|
|
28
|
+
selectedIds: string[],
|
|
29
|
+
filterId: string,
|
|
30
|
+
multiSelect: boolean = false,
|
|
31
|
+
defaultId: string = "all"
|
|
32
|
+
): string[] {
|
|
33
|
+
if (filterId === defaultId) {
|
|
34
|
+
return [defaultId];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (multiSelect) {
|
|
38
|
+
const newIds = selectedIds.filter((id) => id !== defaultId);
|
|
39
|
+
if (newIds.includes(filterId)) {
|
|
40
|
+
const filtered = newIds.filter((id) => id !== filterId);
|
|
41
|
+
return filtered.length === 0 ? [defaultId] : filtered;
|
|
42
|
+
}
|
|
43
|
+
return [...newIds, filterId];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return [filterId];
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/molecules/index.ts
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
// Component exports
|
|
7
7
|
export * from './avatar';
|
|
8
|
+
export * from './bottom-sheet';
|
|
8
9
|
export { FormField, type FormFieldProps } from './FormField';
|
|
9
10
|
export { ListItem, type ListItemProps } from './ListItem';
|
|
10
11
|
export { SearchBar, type SearchBarProps } from './SearchBar';
|
|
11
12
|
export { IconContainer } from './IconContainer';
|
|
12
|
-
export { ScreenHeader, type ScreenHeaderProps } from './ScreenHeader';
|
|
13
13
|
export { BaseModal, type BaseModalProps } from './BaseModal';
|
|
14
14
|
export { ConfirmationModal } from './ConfirmationModalMain';
|
|
15
15
|
export { useConfirmationModal } from './confirmation-modal/useConfirmationModal';
|
|
@@ -25,9 +25,7 @@ export * from './Divider';
|
|
|
25
25
|
export * from "./StepProgress";
|
|
26
26
|
|
|
27
27
|
// Responsive Components
|
|
28
|
-
export { Grid, type GridProps } from './Grid';
|
|
29
28
|
export { List, type ListProps } from './List';
|
|
30
|
-
export { Container, type ContainerProps } from './Container';
|
|
31
29
|
|
|
32
30
|
// Alerts
|
|
33
31
|
export * from './alerts';
|
package/src/organisms/index.ts
CHANGED
|
@@ -11,25 +11,13 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
// Component exports
|
|
14
|
-
export { AppHeader } from './AppHeader';
|
|
15
|
-
export { ScreenLayout } from './ScreenLayout';
|
|
16
14
|
export { FormContainer } from './FormContainer';
|
|
17
|
-
export { FormLayout } from './FormLayout';
|
|
18
15
|
|
|
19
16
|
// Type exports
|
|
20
|
-
export type { AppHeaderProps } from './AppHeader';
|
|
21
|
-
export type { ScreenLayoutProps } from './ScreenLayout';
|
|
22
17
|
export type { FormContainerProps } from './FormContainer';
|
|
23
|
-
export type { FormLayoutProps } from './FormLayout';
|
|
24
18
|
|
|
25
19
|
// Union type for all organism props (used for type narrowing)
|
|
26
|
-
import type { AppHeaderProps } from './AppHeader';
|
|
27
|
-
import type { ScreenLayoutProps } from './ScreenLayout';
|
|
28
20
|
import type { FormContainerProps } from './FormContainer';
|
|
29
|
-
import type { FormLayoutProps } from './FormLayout';
|
|
30
21
|
|
|
31
22
|
export type OrganismComponentProps =
|
|
32
|
-
| { type: '
|
|
33
|
-
| { type: 'ScreenLayout'; props: ScreenLayoutProps }
|
|
34
|
-
| { type: 'FormContainer'; props: FormContainerProps }
|
|
35
|
-
| { type: 'FormLayout'; props: FormLayoutProps };
|
|
23
|
+
| { type: 'FormContainer'; props: FormContainerProps };
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|