rn-inputforge 0.1.2 → 0.3.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 (47) hide show
  1. package/README.md +63 -18
  2. package/lib/commonjs/components/FieldFlowDropdown/FieldFlowDropdown.js +288 -0
  3. package/lib/commonjs/components/FieldFlowDropdown/FieldFlowDropdown.js.map +1 -0
  4. package/lib/commonjs/components/FieldFlowDropdown/index.js +13 -0
  5. package/lib/commonjs/components/FieldFlowDropdown/index.js.map +1 -0
  6. package/lib/commonjs/components/FieldFlowDropdown/styles.js +199 -0
  7. package/lib/commonjs/components/FieldFlowDropdown/styles.js.map +1 -0
  8. package/lib/commonjs/components/FieldFlowDropdown/types.js +6 -0
  9. package/lib/commonjs/components/FieldFlowDropdown/types.js.map +1 -0
  10. package/lib/commonjs/icons/Icon.js +35 -0
  11. package/lib/commonjs/icons/Icon.js.map +1 -1
  12. package/lib/commonjs/index.js +7 -0
  13. package/lib/commonjs/index.js.map +1 -1
  14. package/lib/module/components/FieldFlowDropdown/FieldFlowDropdown.js +283 -0
  15. package/lib/module/components/FieldFlowDropdown/FieldFlowDropdown.js.map +1 -0
  16. package/lib/module/components/FieldFlowDropdown/index.js +4 -0
  17. package/lib/module/components/FieldFlowDropdown/index.js.map +1 -0
  18. package/lib/module/components/FieldFlowDropdown/styles.js +195 -0
  19. package/lib/module/components/FieldFlowDropdown/styles.js.map +1 -0
  20. package/lib/module/components/FieldFlowDropdown/types.js +4 -0
  21. package/lib/module/components/FieldFlowDropdown/types.js.map +1 -0
  22. package/lib/module/icons/Icon.js +35 -0
  23. package/lib/module/icons/Icon.js.map +1 -1
  24. package/lib/module/index.js +1 -0
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/typescript/components/FieldFlowDropdown/FieldFlowDropdown.d.ts +4 -0
  27. package/lib/typescript/components/FieldFlowDropdown/FieldFlowDropdown.d.ts.map +1 -0
  28. package/lib/typescript/components/FieldFlowDropdown/index.d.ts +3 -0
  29. package/lib/typescript/components/FieldFlowDropdown/index.d.ts.map +1 -0
  30. package/lib/typescript/components/FieldFlowDropdown/styles.d.ts +188 -0
  31. package/lib/typescript/components/FieldFlowDropdown/styles.d.ts.map +1 -0
  32. package/lib/typescript/components/FieldFlowDropdown/types.d.ts +51 -0
  33. package/lib/typescript/components/FieldFlowDropdown/types.d.ts.map +1 -0
  34. package/lib/typescript/icons/Icon.d.ts +1 -1
  35. package/lib/typescript/icons/Icon.d.ts.map +1 -1
  36. package/lib/typescript/index.d.ts +3 -1
  37. package/lib/typescript/index.d.ts.map +1 -1
  38. package/lib/typescript/types/index.d.ts +7 -0
  39. package/lib/typescript/types/index.d.ts.map +1 -1
  40. package/package.json +8 -12
  41. package/src/components/FieldFlowDropdown/FieldFlowDropdown.tsx +353 -0
  42. package/src/components/FieldFlowDropdown/index.ts +7 -0
  43. package/src/components/FieldFlowDropdown/styles.ts +193 -0
  44. package/src/components/FieldFlowDropdown/types.ts +57 -0
  45. package/src/icons/Icon.tsx +26 -0
  46. package/src/index.ts +9 -0
  47. package/src/types/index.ts +8 -0
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rn-inputforge",
3
- "version": "0.1.2",
4
- "description": "A production-ready, highly customizable input field system for React Native (bare CLI) — text/email/number/phone/password/search fields, description fields, chat input, built-in validation, theming, icons, and keyboard-aware form navigation.",
3
+ "version": "0.3.0",
4
+ "description": "A production-ready, highly customizable input field system for React Native (bare CLI) — text/email/number/phone/password/search fields, description fields, chat input, and a single/multi-select dropdown (bottom sheet, popup, or inline), with built-in validation, theming, icons, and keyboard-aware form navigation.",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
7
7
  "types": "lib/typescript/index.d.ts",
@@ -33,21 +33,17 @@
33
33
  "form",
34
34
  "form-builder",
35
35
  "chat-input",
36
+ "dropdown",
37
+ "select",
38
+ "multi-select",
39
+ "bottom-sheet",
36
40
  "keyboard-aware",
37
41
  "validation",
38
42
  "typescript",
39
43
  "bare-react-native-cli"
40
44
  ],
41
- "author": "Saad Waheed Khan",
45
+ "author": "Your Name <you@example.com>",
42
46
  "license": "MIT",
43
- "repository": {
44
- "type": "git",
45
- "url": "git+https://github.com/saadwaheed10/rn-inputforge.git"
46
- },
47
- "homepage": "https://github.com/saadwaheed10/rn-inputforge#readme",
48
- "bugs": {
49
- "url": "https://github.com/saadwaheed10/rn-inputforge/issues"
50
- },
51
47
  "peerDependencies": {
52
48
  "react": ">=18.0.0",
53
49
  "react-native": ">=0.73.0"
@@ -59,7 +55,7 @@
59
55
  "prettier": "^3.4.2",
60
56
  "react": "19.2.8",
61
57
  "react-native": "0.86.2",
62
- "react-native-builder-bob": "^0.43.0",
58
+ "react-native-builder-bob": "^0.41.0",
63
59
  "rimraf": "^6.0.1",
64
60
  "typescript": "^5.7.3"
65
61
  },
@@ -0,0 +1,353 @@
1
+ import React, { useMemo, useRef, useState } from 'react';
2
+ import {
3
+ FlatList,
4
+ Modal,
5
+ Pressable,
6
+ Text,
7
+ TextInput,
8
+ View,
9
+ ViewStyle,
10
+ Dimensions,
11
+ } from 'react-native';
12
+ import { useFieldFlowTheme, mergeDeep } from '../../theme/ThemeProvider';
13
+ import { defaultTheme } from '../../theme/defaultTheme';
14
+ import { useFieldState } from '../../hooks/useFieldState';
15
+ import { FieldFlowIcon } from '../../icons/Icon';
16
+ import { createDropdownStyles } from './styles';
17
+ import type { FieldFlowDropdownProps } from './types';
18
+ import type { FieldFlowOption } from '../../types';
19
+
20
+ export function FieldFlowDropdown(props: FieldFlowDropdownProps) {
21
+ const {
22
+ options,
23
+ label,
24
+ placeholder = 'Select...',
25
+ helperText,
26
+ errorText,
27
+ hasError,
28
+ disabled,
29
+ required,
30
+ title,
31
+ presentation = 'bottomSheet',
32
+ searchable,
33
+ searchThreshold = 6,
34
+ searchPlaceholder = 'Search...',
35
+ emptyText = 'No options found',
36
+ maxModalHeight,
37
+ clearable,
38
+ containerStyle,
39
+ triggerStyle,
40
+ labelStyle,
41
+ theme: themeOverride,
42
+ testID,
43
+ multiple,
44
+ } = props;
45
+
46
+ const contextTheme = useFieldFlowTheme();
47
+ const theme = useMemo(
48
+ () =>
49
+ themeOverride
50
+ ? mergeDeep(contextTheme ?? defaultTheme, themeOverride)
51
+ : (contextTheme ?? defaultTheme),
52
+ [contextTheme, themeOverride],
53
+ );
54
+
55
+ const [isOpen, setIsOpen] = useState(false);
56
+ const [query, setQuery] = useState('');
57
+ const [draftValues, setDraftValues] = useState<string[]>(
58
+ multiple ? (props.values ?? []) : [],
59
+ );
60
+ const [anchorLayout, setAnchorLayout] = useState<{
61
+ x: number;
62
+ y: number;
63
+ width: number;
64
+ height: number;
65
+ } | null>(null);
66
+ const triggerRef = useRef<View>(null);
67
+
68
+ const resolvedError = errorText;
69
+ const state = useFieldState({
70
+ isFocused: isOpen,
71
+ hasError: Boolean(hasError || resolvedError),
72
+ disabled,
73
+ });
74
+ const styles = useMemo(
75
+ () => createDropdownStyles(theme, state),
76
+ [theme, state],
77
+ );
78
+
79
+ const selectedValues: string[] = multiple
80
+ ? (props.values ?? [])
81
+ : props.value
82
+ ? [props.value]
83
+ : [];
84
+
85
+ const shouldShowSearch = searchable ?? options.length > searchThreshold;
86
+ const filteredOptions = useMemo(() => {
87
+ if (!query.trim()) return options;
88
+ const q = query.trim().toLowerCase();
89
+ return options.filter(o => o.label.toLowerCase().includes(q));
90
+ }, [options, query]);
91
+
92
+ const openDropdown = () => {
93
+ if (disabled) return;
94
+ if (multiple) setDraftValues(props.values ?? []);
95
+ setQuery('');
96
+ if (presentation === 'inline') {
97
+ triggerRef.current?.measureInWindow((x, y, width, height) => {
98
+ setAnchorLayout({ x, y, width, height });
99
+ setIsOpen(true);
100
+ });
101
+ } else {
102
+ setIsOpen(true);
103
+ }
104
+ };
105
+
106
+ const closeDropdown = () => setIsOpen(false);
107
+
108
+ const commitSingle = (value: string) => {
109
+ if (!multiple) {
110
+ props.onChange(value);
111
+ if (props.closeOnSelect ?? true) closeDropdown();
112
+ }
113
+ };
114
+
115
+ const toggleMultiDraft = (value: string) => {
116
+ setDraftValues(prev =>
117
+ prev.includes(value) ? prev.filter(v => v !== value) : [...prev, value],
118
+ );
119
+ if (multiple && (props.closeOnSelect ?? false)) {
120
+ const next = draftValues.includes(value)
121
+ ? draftValues.filter(v => v !== value)
122
+ : [...draftValues, value];
123
+ props.onChangeMulti(next);
124
+ }
125
+ };
126
+
127
+ const confirmMulti = () => {
128
+ if (multiple) {
129
+ props.onChangeMulti(draftValues);
130
+ }
131
+ closeDropdown();
132
+ };
133
+
134
+ const clearSelection = () => {
135
+ if (multiple) {
136
+ props.onChangeMulti([]);
137
+ setDraftValues([]);
138
+ } else {
139
+ props.onChange(null);
140
+ }
141
+ };
142
+
143
+ const triggerLabel = useMemo(() => {
144
+ if (selectedValues.length === 0) return null;
145
+ const labels = options
146
+ .filter(o => selectedValues.includes(o.value))
147
+ .map(o => o.label);
148
+ if (!multiple) return labels[0] ?? null;
149
+ const maxShown = props.maxLabelsShown ?? 2;
150
+ if (labels.length <= maxShown) return labels.join(', ');
151
+ return `${labels.slice(0, maxShown).join(', ')} +${labels.length - maxShown} more`;
152
+ }, [selectedValues, options, multiple, props]);
153
+
154
+ const renderOption = ({ item }: { item: FieldFlowOption }) => {
155
+ const isSelected = multiple
156
+ ? draftValues.includes(item.value)
157
+ : props.value === item.value;
158
+ const handlePress = () => {
159
+ if (item.disabled) return;
160
+ multiple ? toggleMultiDraft(item.value) : commitSingle(item.value);
161
+ };
162
+ return (
163
+ <Pressable
164
+ onPress={handlePress}
165
+ disabled={item.disabled}
166
+ style={styles.optionRow}
167
+ >
168
+ <View style={{ flex: 1 }}>
169
+ <Text
170
+ style={[
171
+ styles.optionLabel,
172
+ isSelected && styles.optionLabelSelected,
173
+ item.disabled && styles.optionLabelDisabled,
174
+ ]}
175
+ >
176
+ {item.label}
177
+ </Text>
178
+ {item.description ? (
179
+ <Text style={styles.optionDescription}>{item.description}</Text>
180
+ ) : null}
181
+ </View>
182
+ {multiple ? (
183
+ <View style={[styles.checkbox, isSelected && styles.checkboxChecked]}>
184
+ {isSelected ? (
185
+ <FieldFlowIcon name="check" size={12} color="#FFFFFF" />
186
+ ) : null}
187
+ </View>
188
+ ) : isSelected ? (
189
+ <FieldFlowIcon
190
+ name="check"
191
+ size={16}
192
+ color={theme.colors.borderFocused}
193
+ />
194
+ ) : null}
195
+ </Pressable>
196
+ );
197
+ };
198
+
199
+ const listContent = (
200
+ <>
201
+ {shouldShowSearch ? (
202
+ <View style={styles.searchWrapper}>
203
+ <TextInput
204
+ value={query}
205
+ onChangeText={setQuery}
206
+ placeholder={searchPlaceholder}
207
+ placeholderTextColor={theme.colors.placeholder}
208
+ style={styles.searchInput}
209
+ />
210
+ </View>
211
+ ) : null}
212
+ <FlatList
213
+ data={filteredOptions}
214
+ keyExtractor={item => item.value}
215
+ renderItem={renderOption}
216
+ keyboardShouldPersistTaps="handled"
217
+ style={{ maxHeight: maxModalHeight ?? 360 }}
218
+ ListEmptyComponent={<Text style={styles.emptyText}>{emptyText}</Text>}
219
+ />
220
+ {multiple ? (
221
+ <Pressable style={styles.confirmButton} onPress={confirmMulti}>
222
+ <Text style={styles.confirmButtonText}>
223
+ {props.confirmLabel ?? 'Done'}
224
+ </Text>
225
+ </Pressable>
226
+ ) : null}
227
+ </>
228
+ );
229
+
230
+ const header = (
231
+ <View style={styles.modalHeader}>
232
+ <Text style={styles.modalTitle}>{title ?? label ?? 'Select'}</Text>
233
+ {clearable && selectedValues.length > 0 ? (
234
+ <Pressable onPress={clearSelection} hitSlop={8}>
235
+ <Text style={styles.modalClearButton}>Clear</Text>
236
+ </Pressable>
237
+ ) : null}
238
+ </View>
239
+ );
240
+
241
+ return (
242
+ <View style={[styles.wrapper, containerStyle]} testID={testID}>
243
+ {label ? (
244
+ <Text style={[styles.label, labelStyle]}>
245
+ {label}
246
+ {required ? ' *' : ''}
247
+ </Text>
248
+ ) : null}
249
+
250
+ <View ref={triggerRef} collapsable={false}>
251
+ <Pressable
252
+ onPress={openDropdown}
253
+ disabled={disabled}
254
+ style={[styles.trigger, triggerStyle]}
255
+ >
256
+ {triggerLabel ? (
257
+ <Text style={styles.triggerText} numberOfLines={1}>
258
+ {triggerLabel}
259
+ </Text>
260
+ ) : (
261
+ <Text style={styles.placeholderText} numberOfLines={1}>
262
+ {placeholder}
263
+ </Text>
264
+ )}
265
+ <FieldFlowIcon
266
+ name="chevronDown"
267
+ size={16}
268
+ color={theme.colors.icon}
269
+ />
270
+ </Pressable>
271
+ </View>
272
+
273
+ {(helperText || resolvedError) && (
274
+ <View style={styles.footerRow}>
275
+ <View style={{ flex: 1 }}>
276
+ {resolvedError ? (
277
+ <Text style={styles.errorText}>{resolvedError}</Text>
278
+ ) : helperText ? (
279
+ <Text style={styles.helperText}>{helperText}</Text>
280
+ ) : null}
281
+ </View>
282
+ </View>
283
+ )}
284
+
285
+ {presentation === 'inline' ? (
286
+ isOpen && anchorLayout ? (
287
+ <Modal
288
+ transparent
289
+ visible
290
+ animationType="fade"
291
+ onRequestClose={closeDropdown}
292
+ >
293
+ <Pressable style={{ flex: 1 }} onPress={closeDropdown}>
294
+ <View
295
+ style={[styles.inlineDropdown, inlinePosition(anchorLayout)]}
296
+ >
297
+ {listContent}
298
+ </View>
299
+ </Pressable>
300
+ </Modal>
301
+ ) : null
302
+ ) : (
303
+ <Modal
304
+ transparent
305
+ visible={isOpen}
306
+ animationType={presentation === 'popup' ? 'fade' : 'slide'}
307
+ onRequestClose={closeDropdown}
308
+ >
309
+ <Pressable
310
+ style={
311
+ presentation === 'popup'
312
+ ? styles.popupWrapper
313
+ : styles.bottomSheetWrapper
314
+ }
315
+ onPress={closeDropdown}
316
+ >
317
+ <Pressable
318
+ style={
319
+ presentation === 'popup' ? styles.popupCard : styles.sheetCard
320
+ }
321
+ onPress={e => e.stopPropagation()}
322
+ >
323
+ {presentation === 'bottomSheet' ? (
324
+ <View style={styles.dragHandle} />
325
+ ) : null}
326
+ {header}
327
+ {listContent}
328
+ </Pressable>
329
+ </Pressable>
330
+ </Modal>
331
+ )}
332
+ </View>
333
+ );
334
+ }
335
+
336
+ function inlinePosition(anchor: {
337
+ x: number;
338
+ y: number;
339
+ width: number;
340
+ height: number;
341
+ }): ViewStyle {
342
+ const screenHeight = Dimensions.get('window').height;
343
+ const spaceBelow = screenHeight - (anchor.y + anchor.height);
344
+ const openUpward = spaceBelow < 260;
345
+ return {
346
+ left: anchor.x,
347
+ width: anchor.width,
348
+ maxHeight: 300,
349
+ ...(openUpward
350
+ ? { bottom: screenHeight - anchor.y + 4 }
351
+ : { top: anchor.y + anchor.height + 4 }),
352
+ };
353
+ }
@@ -0,0 +1,7 @@
1
+ export { FieldFlowDropdown } from './FieldFlowDropdown';
2
+ export type {
3
+ FieldFlowDropdownProps,
4
+ FieldFlowDropdownSingleProps,
5
+ FieldFlowDropdownMultiProps,
6
+ FieldFlowDropdownPresentation,
7
+ } from './types';
@@ -0,0 +1,193 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import type { FieldFlowState, FieldFlowTheme } from '../../types';
3
+ import { borderColorFor, labelColorFor } from '../FieldFlowInput/styles';
4
+
5
+ export function createDropdownStyles(theme: FieldFlowTheme, state: FieldFlowState) {
6
+ return StyleSheet.create({
7
+ wrapper: {
8
+ marginBottom: theme.spacing.gap * 2,
9
+ },
10
+ label: {
11
+ fontSize: theme.typography.labelFontSize,
12
+ marginBottom: theme.spacing.gap / 1.5,
13
+ color: labelColorFor(theme, state),
14
+ fontWeight: '500',
15
+ },
16
+ trigger: {
17
+ flexDirection: 'row',
18
+ alignItems: 'center',
19
+ backgroundColor: state === 'disabled' ? theme.colors.backgroundDisabled : theme.colors.background,
20
+ borderRadius: theme.spacing.borderRadius,
21
+ borderWidth: state === 'focused' ? theme.spacing.borderWidthFocused : theme.spacing.borderWidth,
22
+ borderColor: borderColorFor(theme, state),
23
+ paddingHorizontal: theme.spacing.paddingHorizontal,
24
+ minHeight: theme.spacing.paddingVertical * 2 + theme.typography.fontSize + 6,
25
+ },
26
+ triggerText: {
27
+ flex: 1,
28
+ fontSize: theme.typography.fontSize,
29
+ color: theme.colors.text,
30
+ },
31
+ placeholderText: {
32
+ flex: 1,
33
+ fontSize: theme.typography.fontSize,
34
+ color: theme.colors.placeholder,
35
+ },
36
+ footerRow: {
37
+ flexDirection: 'row',
38
+ justifyContent: 'space-between',
39
+ marginTop: theme.spacing.gap / 1.5,
40
+ },
41
+ errorText: {
42
+ fontSize: theme.typography.helperFontSize,
43
+ color: theme.colors.error,
44
+ flexShrink: 1,
45
+ },
46
+ helperText: {
47
+ fontSize: theme.typography.helperFontSize,
48
+ color: theme.colors.label,
49
+ flexShrink: 1,
50
+ },
51
+
52
+ // Modal
53
+ backdrop: {
54
+ flex: 1,
55
+ backgroundColor: 'rgba(0,0,0,0.4)',
56
+ },
57
+ bottomSheetWrapper: {
58
+ flex: 1,
59
+ justifyContent: 'flex-end',
60
+ },
61
+ popupWrapper: {
62
+ flex: 1,
63
+ justifyContent: 'center',
64
+ alignItems: 'center',
65
+ paddingHorizontal: 24,
66
+ },
67
+ sheetCard: {
68
+ backgroundColor: theme.colors.background,
69
+ borderTopLeftRadius: theme.spacing.borderRadius * 1.6,
70
+ borderTopRightRadius: theme.spacing.borderRadius * 1.6,
71
+ paddingTop: theme.spacing.gap,
72
+ paddingBottom: theme.spacing.paddingVertical,
73
+ },
74
+ popupCard: {
75
+ width: '100%',
76
+ backgroundColor: theme.colors.background,
77
+ borderRadius: theme.spacing.borderRadius * 1.4,
78
+ paddingTop: theme.spacing.gap,
79
+ paddingBottom: theme.spacing.paddingVertical,
80
+ maxHeight: '80%',
81
+ },
82
+ dragHandle: {
83
+ alignSelf: 'center',
84
+ width: 36,
85
+ height: 4,
86
+ borderRadius: 2,
87
+ backgroundColor: theme.colors.border,
88
+ marginBottom: theme.spacing.gap,
89
+ },
90
+ modalHeader: {
91
+ flexDirection: 'row',
92
+ alignItems: 'center',
93
+ justifyContent: 'space-between',
94
+ paddingHorizontal: theme.spacing.paddingHorizontal,
95
+ paddingBottom: theme.spacing.gap,
96
+ borderBottomWidth: theme.spacing.borderWidth,
97
+ borderBottomColor: theme.colors.border,
98
+ marginBottom: theme.spacing.gap,
99
+ },
100
+ modalTitle: {
101
+ fontSize: theme.typography.fontSize + 1,
102
+ fontWeight: '600',
103
+ color: theme.colors.text,
104
+ },
105
+ modalClearButton: {
106
+ fontSize: theme.typography.helperFontSize,
107
+ color: theme.colors.borderFocused,
108
+ },
109
+ searchWrapper: {
110
+ paddingHorizontal: theme.spacing.paddingHorizontal,
111
+ marginBottom: theme.spacing.gap,
112
+ },
113
+ searchInput: {
114
+ backgroundColor: theme.colors.backgroundDisabled,
115
+ borderRadius: theme.spacing.borderRadius,
116
+ paddingHorizontal: theme.spacing.paddingHorizontal,
117
+ paddingVertical: theme.spacing.paddingVertical * 0.6,
118
+ fontSize: theme.typography.fontSize,
119
+ color: theme.colors.text,
120
+ },
121
+ optionRow: {
122
+ flexDirection: 'row',
123
+ alignItems: 'center',
124
+ justifyContent: 'space-between',
125
+ paddingHorizontal: theme.spacing.paddingHorizontal,
126
+ paddingVertical: theme.spacing.paddingVertical * 0.9,
127
+ },
128
+ optionLabel: {
129
+ fontSize: theme.typography.fontSize,
130
+ color: theme.colors.text,
131
+ },
132
+ optionLabelSelected: {
133
+ color: theme.colors.borderFocused,
134
+ fontWeight: '600',
135
+ },
136
+ optionLabelDisabled: {
137
+ color: theme.colors.placeholder,
138
+ },
139
+ optionDescription: {
140
+ fontSize: theme.typography.helperFontSize,
141
+ color: theme.colors.placeholder,
142
+ marginTop: 2,
143
+ },
144
+ checkbox: {
145
+ width: 20,
146
+ height: 20,
147
+ borderRadius: 5,
148
+ borderWidth: theme.spacing.borderWidth,
149
+ borderColor: theme.colors.border,
150
+ alignItems: 'center',
151
+ justifyContent: 'center',
152
+ },
153
+ checkboxChecked: {
154
+ backgroundColor: theme.colors.borderFocused,
155
+ borderColor: theme.colors.borderFocused,
156
+ },
157
+ emptyText: {
158
+ textAlign: 'center',
159
+ paddingVertical: theme.spacing.paddingVertical * 2,
160
+ color: theme.colors.placeholder,
161
+ fontSize: theme.typography.fontSize,
162
+ },
163
+ confirmButton: {
164
+ marginHorizontal: theme.spacing.paddingHorizontal,
165
+ marginTop: theme.spacing.gap,
166
+ backgroundColor: theme.colors.borderFocused,
167
+ borderRadius: theme.spacing.borderRadius,
168
+ paddingVertical: theme.spacing.paddingVertical * 0.8,
169
+ alignItems: 'center',
170
+ },
171
+ confirmButtonText: {
172
+ color: '#FFFFFF',
173
+ fontSize: theme.typography.fontSize,
174
+ fontWeight: '600',
175
+ },
176
+
177
+ // Inline presentation
178
+ inlineDropdown: {
179
+ position: 'absolute',
180
+ backgroundColor: theme.colors.background,
181
+ borderRadius: theme.spacing.borderRadius,
182
+ borderWidth: theme.spacing.borderWidth,
183
+ borderColor: theme.colors.border,
184
+ shadowColor: '#000',
185
+ shadowOffset: { width: 0, height: 4 },
186
+ shadowOpacity: 0.15,
187
+ shadowRadius: 8,
188
+ elevation: 6,
189
+ overflow: 'hidden',
190
+ zIndex: 50,
191
+ },
192
+ });
193
+ }
@@ -0,0 +1,57 @@
1
+ import type { ViewStyle, TextStyle } from 'react-native';
2
+ import type { DeepPartial, FieldFlowOption, FieldFlowTheme } from '../../types';
3
+
4
+ export type FieldFlowDropdownPresentation = 'bottomSheet' | 'popup' | 'inline';
5
+
6
+ interface BaseDropdownProps {
7
+ options: FieldFlowOption[];
8
+ label?: string;
9
+ placeholder?: string;
10
+ helperText?: string;
11
+ errorText?: string;
12
+ hasError?: boolean;
13
+ disabled?: boolean;
14
+ required?: boolean;
15
+
16
+ /** Modal title shown at the top of the bottom sheet / popup. Defaults to `label` if omitted. */
17
+ title?: string;
18
+ /** 'bottomSheet' slides up from the bottom, 'popup' fades in centered, 'inline' drops down under the field. Default: 'bottomSheet'. */
19
+ presentation?: FieldFlowDropdownPresentation;
20
+ /** Shows a search box above the option list once there are more than this many options. Default: 6. Set to false to disable search entirely. */
21
+ searchable?: boolean;
22
+ searchThreshold?: number;
23
+ searchPlaceholder?: string;
24
+ emptyText?: string;
25
+
26
+ maxModalHeight?: number;
27
+ clearable?: boolean;
28
+
29
+ containerStyle?: ViewStyle;
30
+ triggerStyle?: ViewStyle;
31
+ labelStyle?: TextStyle;
32
+ helperStyle?: TextStyle;
33
+ theme?: DeepPartial<FieldFlowTheme>;
34
+ testID?: string;
35
+ }
36
+
37
+ export interface FieldFlowDropdownSingleProps extends BaseDropdownProps {
38
+ multiple?: false;
39
+ value?: string | null;
40
+ onChange: (value: string | null) => void;
41
+ /** Close the sheet as soon as an option is tapped. Default: true. */
42
+ closeOnSelect?: boolean;
43
+ }
44
+
45
+ export interface FieldFlowDropdownMultiProps extends BaseDropdownProps {
46
+ multiple: true;
47
+ values?: string[];
48
+ onChangeMulti: (values: string[]) => void;
49
+ /** Close the sheet as soon as an option is tapped. Default: false (user taps "Done"). */
50
+ closeOnSelect?: boolean;
51
+ /** Label for the confirm button shown in multi-select mode. Default: "Done". */
52
+ confirmLabel?: string;
53
+ /** Max number of individual labels to show on the trigger before collapsing to "N selected". Default: 2. */
54
+ maxLabelsShown?: number;
55
+ }
56
+
57
+ export type FieldFlowDropdownProps = FieldFlowDropdownSingleProps | FieldFlowDropdownMultiProps;