aport-tools 4.5.2 → 4.6.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 (38) hide show
  1. package/package.json +13 -28
  2. package/src/cards/Card.tsx +129 -0
  3. package/src/cards/index.ts +1 -0
  4. package/src/components/Button.tsx +180 -0
  5. package/src/fonts/Text.tsx +137 -0
  6. package/{dist/fonts/index.d.ts → src/fonts/index.ts} +1 -0
  7. package/src/forms/ErrorList.tsx +47 -0
  8. package/src/forms/FORMDOC.md +87 -0
  9. package/{dist/forms/Form.d.ts → src/forms/Form.tsx} +2 -0
  10. package/src/forms/FormContext.tsx +248 -0
  11. package/src/forms/Input.tsx +174 -0
  12. package/src/forms/InputAttach.tsx +184 -0
  13. package/src/forms/InputCheck.tsx +169 -0
  14. package/src/forms/InputList.tsx +304 -0
  15. package/src/forms/Label.tsx +26 -0
  16. package/src/forms/Stepper.tsx +289 -0
  17. package/src/forms/TextArea.tsx +91 -0
  18. package/{dist/forms/index.d.ts → src/forms/index.ts} +4 -2
  19. package/src/index.ts +6 -0
  20. package/dist/cards/Card.d.ts +0 -57
  21. package/dist/cards/index.d.ts +0 -1
  22. package/dist/components/Button.d.ts +0 -52
  23. package/dist/fonts/Text.d.ts +0 -64
  24. package/dist/forms/ErrorList.d.ts +0 -6
  25. package/dist/forms/FormContext.d.ts +0 -132
  26. package/dist/forms/Input.d.ts +0 -43
  27. package/dist/forms/InputAttach.d.ts +0 -16
  28. package/dist/forms/InputCheck.d.ts +0 -19
  29. package/dist/forms/InputList.d.ts +0 -93
  30. package/dist/forms/Label.d.ts +0 -7
  31. package/dist/forms/Stepper.d.ts +0 -54
  32. package/dist/forms/TextArea.d.ts +0 -13
  33. package/dist/index.d.ts +0 -4
  34. package/dist/index.esm.js +0 -1493
  35. package/dist/index.esm.js.map +0 -1
  36. package/dist/index.js +0 -1526
  37. package/dist/index.js.map +0 -1
  38. /package/{dist/buttons/index.d.ts → src/buttons/index.ts} +0 -0
@@ -0,0 +1,169 @@
1
+ import React, { useContext, useEffect, useRef, useState } from "react";
2
+ import { useFormContext } from "../forms/FormContext";
3
+ import { Image, StyleSheet, FlatList } from "react-native";
4
+ import { ThemeContext } from "aport-themes";
5
+ import { Text } from "../fonts";
6
+ import { Card } from "../cards";
7
+ import ErrorList from "./ErrorList";
8
+
9
+ interface InputOption {
10
+ id: string;
11
+ label?: string;
12
+ icon?: string;
13
+ value: string;
14
+ }
15
+
16
+ interface InputCheckProps {
17
+ name: string;
18
+ options: InputOption[]; // Use the shared interface
19
+ multi?: boolean; // Allows multiple selection (checkbox behavior)
20
+ max?: number; // Maximum number of selections
21
+ rowAmount?: number; // Number of items per row
22
+ iconPosition?: "row" | "column"; // Icon and label layout
23
+ disabled?: boolean; // Disable the input
24
+ firstValue?: InputOption[]; // Use the shared interface
25
+ }
26
+
27
+ const InputCheck: React.FC<InputCheckProps> = ({
28
+ name,
29
+ options,
30
+ multi = false,
31
+ max,
32
+ rowAmount = 3,
33
+ firstValue,
34
+ iconPosition = "row",
35
+ disabled = false,
36
+ }) => {
37
+ const { formValues, setFormValue, errors: formErrors } = useFormContext();
38
+ const [selectedValues, setSelectedValues] = useState<InputOption[]>([]);
39
+ const { theme } = useContext(ThemeContext);
40
+ const { colors } = theme;
41
+ const isFirstRender = useRef(true);
42
+
43
+ // Initialize selectedValues on first render if firstValue is provided
44
+ useEffect(() => {
45
+ if (isFirstRender.current && firstValue) {
46
+ const initialSelectedValues = options.filter((option) =>
47
+ firstValue.some((fv) => fv.value === option.value)
48
+ );
49
+
50
+ setSelectedValues(initialSelectedValues);
51
+ setFormValue(name, initialSelectedValues, initialSelectedValues); // Update form context
52
+ isFirstRender.current = false; // Prevent subsequent updates
53
+ }
54
+ }, [firstValue, name, options, setFormValue]);
55
+
56
+ const handleSelect = (id: string, value: string) => {
57
+ if (disabled) return;
58
+
59
+ let updatedSelection;
60
+ if (multi) {
61
+ const alreadySelected = selectedValues.find((item) => item.id === id);
62
+ if (alreadySelected) {
63
+ updatedSelection = selectedValues.filter((item) => item.id !== id);
64
+ } else {
65
+ if (max && selectedValues.length >= max) return; // Prevent selection beyond max
66
+ updatedSelection = [...selectedValues, { id, value }];
67
+ }
68
+ } else {
69
+ updatedSelection = [{ id, value }];
70
+ }
71
+
72
+ setSelectedValues(updatedSelection);
73
+ setFormValue(name, updatedSelection);
74
+ };
75
+
76
+ const renderItem = ({ item }: { item: InputOption }) => {
77
+ const isSelected = selectedValues.some(
78
+ (selected) => selected.id === item.id.toString()
79
+ );
80
+
81
+ return (
82
+ <Card
83
+ pressable
84
+ onPress={() => handleSelect(item.id.toString(), item.value)}
85
+ style={[
86
+ styles.card,
87
+ isSelected && { backgroundColor: colors.primary.hex }, // Replace with colors.primary.hex if available
88
+ disabled && styles.cardDisabled,
89
+ ]}
90
+ >
91
+ {item.icon && (
92
+ <Image
93
+ source={{ uri: item.icon }}
94
+ style={[
95
+ styles.icon,
96
+ iconPosition === "column" && styles.iconColumn,
97
+ ]}
98
+ />
99
+ )}
100
+ {item.label && (
101
+ <Text
102
+ style={[
103
+ styles.label,
104
+ { color: isSelected ? colors.textButton.hex : colors.text.hex },
105
+ ]}
106
+ >
107
+ {item.label}
108
+ </Text>
109
+ )}
110
+ </Card>
111
+ );
112
+ };
113
+
114
+ return (
115
+ <>
116
+ {/* Display the name as a title */}
117
+ <Text style={styles.title}>{name}</Text>
118
+
119
+ <FlatList
120
+ data={options}
121
+ renderItem={renderItem}
122
+ keyExtractor={(item) => item.id}
123
+ numColumns={rowAmount}
124
+ columnWrapperStyle={rowAmount > 1 ? styles.row : undefined}
125
+ scrollEnabled={false}
126
+ />
127
+ {formErrors[name] && formErrors[name].length > 0 && (
128
+ <ErrorList errors={formErrors[name]} />
129
+ )}
130
+ </>
131
+ );
132
+ };
133
+
134
+ export default InputCheck;
135
+
136
+ const styles = StyleSheet.create({
137
+ card: {
138
+ flex: 1,
139
+ margin: 5,
140
+ padding: 10,
141
+ borderWidth: 1,
142
+ borderColor: "#ccc",
143
+ borderRadius: 8,
144
+ alignItems: "center",
145
+ justifyContent: "center",
146
+ },
147
+ title: {
148
+ marginBottom: 4,
149
+ fontSize: 14,
150
+ },
151
+ cardDisabled: {
152
+ opacity: 0.5,
153
+ },
154
+ icon: {
155
+ width: 40,
156
+ height: 40,
157
+ marginBottom: 5,
158
+ },
159
+ iconColumn: {
160
+ marginBottom: 10,
161
+ },
162
+ label: {
163
+ fontSize: 14,
164
+ textAlign: "center",
165
+ },
166
+ row: {
167
+ justifyContent: "space-between",
168
+ },
169
+ });
@@ -0,0 +1,304 @@
1
+ import React, { useState, useCallback, useContext, useEffect, useMemo } from 'react';
2
+ import { View, Text, TouchableOpacity, FlatList, StyleSheet, Modal, Pressable, Keyboard } from 'react-native';
3
+ import { ThemeContext } from "aport-themes";
4
+ import { useFormContext } from '../forms/FormContext';
5
+ import ErrorList from "./ErrorList";
6
+
7
+ interface Option {
8
+ id: number | string;
9
+ label: string;
10
+ value: any;
11
+ }
12
+
13
+ interface InputListProps {
14
+ name: string;
15
+ /**
16
+ * Placeholder text displayed in the input box when no selection is made.
17
+ * @type {string}
18
+ * @default "Choose value/s"
19
+ */
20
+
21
+ /**
22
+ * Optional first value if you want to set values with fetch or dont have empty inputs.
23
+ */
24
+ firstValue?: any[]; // Optional prop for the initial value
25
+
26
+ placeholder?: string;
27
+
28
+ /**
29
+ * Custom styles for the component.
30
+ * @type {object}
31
+ */
32
+ style?: object;
33
+
34
+ /**
35
+ * Array of options to display in the dropdown. Each option should have an id, label, and value.
36
+ * @type {Option[]}
37
+ */
38
+ options: Option[];
39
+
40
+ /**
41
+ * Enables multi-selection mode when true. If enabled, users can select multiple values.
42
+ * @type {boolean}
43
+ * @default false
44
+ */
45
+ multi?: boolean;
46
+
47
+ /**
48
+ * Disables the dropdown input when true.
49
+ * @type {boolean}
50
+ * @default false
51
+ */
52
+ disabled?: boolean;
53
+
54
+ /**
55
+ * Key used to sort options in the dropdown (e.g., by 'id', 'label').
56
+ * @type {keyof Option}
57
+ */
58
+ sortBy?: keyof Option;
59
+
60
+ /**
61
+ * If true, displays a separator line between each option.
62
+ * @type {boolean}
63
+ * @default false
64
+ */
65
+ separator?: boolean;
66
+
67
+ /**
68
+ * Closes the dropdown if the user scrolls the list.
69
+ * @type {boolean}
70
+ * @default false
71
+ */
72
+ closeOnScroll?: boolean;
73
+
74
+ /**
75
+ * Closes the dropdown after selecting an item if true (only relevant when multi is false).
76
+ * @type {boolean}
77
+ * @default true
78
+ */
79
+ closeOnSelect?: boolean;
80
+
81
+ /**
82
+ * Limits the maximum number of items that can be selected when multi is true.
83
+ * Once the limit is reached, the dropdown closes, and no further selections can be made until
84
+ * an item is deselected.
85
+ * @type {number}
86
+ */
87
+ maxSelection?: number;
88
+
89
+ /**
90
+ * Callback function called when the selection changes. Useful for conditional field enabling.
91
+ * @param {Option | Option[] | null} selection - The updated selection.
92
+ */
93
+ onChange?: (selection: Option | Option[] | null) => void;
94
+ }
95
+
96
+ /**
97
+ * InputList component - A custom dropdown list component for React Native with multi-selection support,
98
+ * customizable styling, sorting, and configurable close behavior on selection or scrolling.
99
+ *
100
+ * @param {string} placeholder - Placeholder text for the input.
101
+ * @param {object} style - Custom styles for the component.
102
+ * @param {Option[]} options - Array of options to display in the dropdown.
103
+ * @param {boolean} multi - Enables multi-selection mode.
104
+ * @param {boolean} disabled - Disables the dropdown input.
105
+ * @param {keyof Option} sortBy - Key to sort options by (e.g., 'id').
106
+ * @param {boolean} separator - If true, adds a separator line between options.
107
+ * @param {boolean} closeOnScroll - Closes the dropdown if the user scrolls the list.
108
+ * @param {boolean} closeOnSelect - Closes the dropdown on selection in single-select mode.
109
+ * @param {number} maxSelection - Maximum number of items selectable in multi-select mode.
110
+ */
111
+ export const InputList: React.FC<InputListProps> = ({
112
+ name,
113
+ placeholder = "Choose value/s",
114
+ style,
115
+ options,
116
+ multi = false,
117
+ disabled = false,
118
+ sortBy,
119
+ firstValue = [],
120
+ separator = false,
121
+ closeOnScroll = false,
122
+ closeOnSelect = true,
123
+ maxSelection,
124
+ onChange,
125
+ }) => {
126
+ const { formValues, setFormValue, errors: formErrors } = useFormContext();
127
+ const [isDropdownVisible, setIsDropdownVisible] = useState(false);
128
+
129
+ // Memoize sorted options for performance
130
+ const sortedOptions = useMemo(() => {
131
+ return sortBy
132
+ ? [...options].sort((a, b) => (a[sortBy] > b[sortBy] ? 1 : -1))
133
+ : options;
134
+ }, [options, sortBy]);
135
+ const { theme } = useContext(ThemeContext);
136
+ const { colors } = theme;
137
+
138
+ // Initialize selected options based on firstValue
139
+ // Filter initial selections from options based on `firstValue`
140
+ const initialSelections = useMemo(
141
+ () => options.filter((opt) => firstValue.includes(opt.value)),
142
+ [options, firstValue]
143
+ );
144
+ // State for selected options
145
+ const [selectedOptions, setSelectedOptions] = useState(
146
+ multi ? initialSelections : initialSelections[0] || null
147
+ );
148
+ // Update form value when firstValue changes
149
+ useEffect(() => {
150
+ if (!firstValue || firstValue.length === 0) return;
151
+
152
+ const matchedSelections = options.filter((opt) => firstValue.includes(opt.value));
153
+
154
+ if (multi) {
155
+ setSelectedOptions(matchedSelections);
156
+ setFormValue(name, matchedSelections, matchedSelections);
157
+ } else {
158
+ const singleValue = matchedSelections[0] || null;
159
+ setSelectedOptions(singleValue);
160
+ setFormValue(name, singleValue, singleValue);
161
+ }
162
+ }, [firstValue, multi]);
163
+
164
+
165
+ // Handle option selection
166
+ const handleSelectOption = (option: Option) => {
167
+
168
+ let updatedSelections;
169
+
170
+ if (multi) {
171
+ const selectedArray = Array.isArray(selectedOptions) ? selectedOptions : [];
172
+ const alreadySelected = selectedArray.some((opt) => opt.id === option.id);
173
+
174
+ updatedSelections = alreadySelected
175
+ ? selectedArray.filter((opt) => opt.id !== option.id)
176
+ : [...selectedArray, option];
177
+
178
+ if (!alreadySelected && maxSelection && updatedSelections.length >= maxSelection) {
179
+ setIsDropdownVisible(false);
180
+ }
181
+
182
+ setFormValue(name, updatedSelections);
183
+ } else {
184
+ updatedSelections = option;
185
+ setFormValue(name, option);
186
+ if (closeOnSelect) setIsDropdownVisible(false);
187
+ }
188
+
189
+ setSelectedOptions(updatedSelections);
190
+ if (onChange) onChange(updatedSelections);
191
+ };
192
+
193
+ const isItemDisabled = (option: Option) => {
194
+ if (!multi) return false; // Disable check only applies for multi-select
195
+
196
+ // Ensure selectedOptions is treated as an array
197
+ const selectedArray = Array.isArray(selectedOptions) ? selectedOptions : [];
198
+ return (
199
+ maxSelection &&
200
+ selectedArray.length >= maxSelection &&
201
+ !selectedArray.some((opt) => opt.id === option.id)
202
+ );
203
+ };
204
+
205
+ const renderSelectedText = () => {
206
+ if (multi) {
207
+ // Ensure selectedOptions is treated as an array
208
+ const selectedArray = Array.isArray(selectedOptions) ? selectedOptions : [];
209
+ return selectedArray.map((opt) => opt.label).join(', ') || placeholder;
210
+ }
211
+ return (selectedOptions as Option)?.label || placeholder;
212
+ };
213
+
214
+
215
+ const toggleDropdown = () => {
216
+ if (!disabled) {
217
+ setIsDropdownVisible(!isDropdownVisible);
218
+ if (!isDropdownVisible) Keyboard.dismiss();
219
+ }
220
+ };
221
+
222
+ const handleCloseDropdown = useCallback(() => {
223
+ if (isDropdownVisible) setIsDropdownVisible(false);
224
+ }, [isDropdownVisible]);
225
+
226
+ /**
227
+ * Renders selected options as a comma-separated string or the placeholder if none selected.
228
+ * @returns {string} - The display text for selected options or placeholder.
229
+ */
230
+
231
+
232
+ // Conditionally render item as disabled if max selection reached and item is unselected
233
+
234
+
235
+ return (
236
+ <View style={[styles.container, style]}>
237
+ <Text style={{color: colors.text.hex,marginBottom: 4,
238
+ }}>{name}</Text>
239
+
240
+ <TouchableOpacity style={[styles.inputContainer,{borderColor: formErrors[name] ? colors.error.hex : "#CCC",}]} onPress={toggleDropdown} disabled={disabled}>
241
+ <Text style={{color: colors.text.hex}}>{renderSelectedText()}</Text>
242
+ </TouchableOpacity>
243
+
244
+ {/* Display errors only if form has been submitted and there are errors */}
245
+ {formErrors[name] && formErrors[name].length > 0 && (
246
+ <ErrorList errors={formErrors[name]} />
247
+ )}
248
+ <Modal visible={isDropdownVisible} transparent animationType="fade">
249
+ <Pressable style={styles.overlay} onPress={handleCloseDropdown} />
250
+ <View style={[styles.dropdownContainer,{backgroundColor: colors.body.hex,
251
+ }]}>
252
+ <FlatList
253
+ data={sortedOptions}
254
+ keyExtractor={(item) => item.id.toString()}
255
+ renderItem={({ item }) => {
256
+ const isSelected = multi
257
+ ? Array.isArray(selectedOptions) && selectedOptions.some((opt: Option) => opt.id === item.id)
258
+ : selectedOptions && 'id' in selectedOptions && selectedOptions.id === item.id;
259
+
260
+ const isDisabled = isItemDisabled(item);
261
+
262
+ return (
263
+ <TouchableOpacity
264
+ onPress={() => handleSelectOption(item)}
265
+ style={[
266
+ styles.optionItem,
267
+ isSelected ? {backgroundColor: colors.primary.hex} : {},
268
+ isDisabled ? {backgroundColor: colors.placeHolder.hex} : {},
269
+ ]}
270
+ disabled={!!isDisabled} // Ensure disabled prop is a boolean
271
+ >
272
+ <Text style={[isSelected ? {color: colors.body.hex}: {color: colors.text.hex}, isDisabled ? styles.disabledText : {}]}>
273
+ {item.label}
274
+ </Text>
275
+ </TouchableOpacity>
276
+ );
277
+ }}
278
+ ItemSeparatorComponent={() => (separator ? <View style={styles.separator} /> : null)}
279
+ scrollEnabled={!closeOnScroll}
280
+ />
281
+ </View>
282
+ </Modal>
283
+ </View>
284
+ );
285
+ };
286
+
287
+ const styles = StyleSheet.create({
288
+ container: {marginVertical: 10 },
289
+ inputContainer: { padding: 12, borderWidth: 1, borderRadius: 5 },
290
+ dropdownContainer: {
291
+ position: 'absolute',
292
+ top: '30%', // Center the dropdown vertically
293
+ alignSelf: 'center',
294
+ width: '90%',
295
+ backgroundColor: '#fff',
296
+ borderRadius: 8,
297
+ elevation: 5,
298
+ paddingVertical: 10,
299
+ },
300
+ optionItem: { padding: 12 },
301
+ disabledText: { color: '#999' },
302
+ separator: { height: 1, backgroundColor: '#ddd', marginHorizontal: 8 },
303
+ overlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.3)' },
304
+ });
@@ -0,0 +1,26 @@
1
+ // src/forms/Label.tsx
2
+
3
+ import React, { useContext } from 'react';
4
+ import { StyleSheet } from 'react-native';
5
+ import { Text } from '../fonts/Text';
6
+ import { ThemeContext } from 'aport-themes';
7
+
8
+ interface LabelProps {
9
+ text: string;
10
+ style?: any;
11
+ }
12
+
13
+ const Label: React.FC<LabelProps> = ({ text, style }) => {
14
+ const { theme } = useContext(ThemeContext);
15
+ const { colors } = theme;
16
+ return <Text style={[styles.label, style, { color: colors.text.hex }]}>{text}</Text>;
17
+ };
18
+
19
+ const styles = StyleSheet.create({
20
+ label: {
21
+ marginBottom: 4,
22
+ fontWeight: '500',
23
+ },
24
+ });
25
+
26
+ export default Label;