@nova-ui-native/core 1.0.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.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # nova-ui-native
@@ -0,0 +1,23 @@
1
+ import { StyleProp, ViewStyle } from 'react-native';
2
+ type FilterMode = 'local' | 'api';
3
+ interface AutocompleteProps<T> {
4
+ data: T[];
5
+ valueKey: keyof T;
6
+ filterKey: keyof T;
7
+ value: string | number | null;
8
+ onSelect: (value: string | number | null) => void;
9
+ filterMode?: FilterMode;
10
+ onSearch?: (query: string) => void;
11
+ minSearchLength?: number;
12
+ placeholder?: string;
13
+ label?: string;
14
+ helperText?: string;
15
+ error?: boolean;
16
+ loading?: boolean;
17
+ containerStyle?: StyleProp<ViewStyle>;
18
+ visibleItems?: number;
19
+ maxResults?: number;
20
+ heightItem?: number;
21
+ }
22
+ export default function CustomAutocomplete<T extends object>({ data, value, onSelect, valueKey, filterKey, filterMode, onSearch, minSearchLength, placeholder, label, helperText, error, loading, containerStyle, visibleItems, maxResults, heightItem }: AutocompleteProps<T>): import("react").JSX.Element;
23
+ export {};
@@ -0,0 +1,346 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = CustomAutocomplete;
4
+ const react_1 = require("react");
5
+ const react_native_1 = require("react-native");
6
+ const ITEM_HEIGHT = 62;
7
+ function normalize(text) {
8
+ return text.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
9
+ }
10
+ function CustomAutocomplete({ data, value, onSelect, valueKey, filterKey, filterMode = 'local', onSearch, minSearchLength = 1, placeholder = 'Buscar...', label, helperText, error = false, loading = false, containerStyle, visibleItems = 4, maxResults = 200, heightItem = 0 }) {
11
+ let heightItemC = heightItem + ITEM_HEIGHT;
12
+ const [inputValue, setInputValue] = (0, react_1.useState)('');
13
+ const [filteredData, setFilteredData] = (0, react_1.useState)([]);
14
+ const [isOpen, setIsOpen] = (0, react_1.useState)(false);
15
+ const inputRef = (0, react_1.useRef)(null);
16
+ (0, react_1.useEffect)(() => {
17
+ if (value !== undefined && value !== null && value !== '') {
18
+ const match = data.find((item) => String(item[valueKey]) === String(value));
19
+ if (match) {
20
+ setInputValue(String(match[filterKey]));
21
+ return;
22
+ }
23
+ }
24
+ setInputValue('');
25
+ }, [value, data, valueKey, filterKey]);
26
+ (0, react_1.useEffect)(() => {
27
+ if (filterMode === 'api') {
28
+ setFilteredData(data.slice(0, maxResults));
29
+ return;
30
+ }
31
+ const trimmed = inputValue.trim();
32
+ if (trimmed === '') {
33
+ setFilteredData(data.slice(0, maxResults));
34
+ return;
35
+ }
36
+ const currentMatch = data.find((item) => String(item[valueKey]) === String(value));
37
+ if (currentMatch && inputValue === String(currentMatch[filterKey])) {
38
+ setFilteredData(data.slice(0, maxResults));
39
+ return;
40
+ }
41
+ const query = normalize(trimmed);
42
+ setFilteredData(data
43
+ .filter((item) => normalize(String(item[filterKey])).includes(query))
44
+ .slice(0, maxResults));
45
+ }, [inputValue, data, filterKey, value, valueKey, maxResults, filterMode]);
46
+ (0, react_1.useEffect)(() => {
47
+ if (filterMode === 'api') {
48
+ setFilteredData(data.slice(0, maxResults));
49
+ }
50
+ }, [data, filterMode, maxResults]);
51
+ const handleChangeText = (text) => {
52
+ setInputValue(text);
53
+ setIsOpen(true);
54
+ if (filterMode === 'api' && onSearch) {
55
+ if (text.trim().length >= minSearchLength) {
56
+ onSearch(text.trim());
57
+ }
58
+ else if (text.trim().length === 0) {
59
+ onSearch('');
60
+ }
61
+ }
62
+ };
63
+ const handleSelect = (item) => {
64
+ setInputValue(String(item[filterKey]));
65
+ onSelect(item[valueKey]);
66
+ setIsOpen(false);
67
+ inputRef.current?.blur();
68
+ };
69
+ const handleClear = () => {
70
+ setInputValue('');
71
+ onSelect(null);
72
+ setIsOpen(false);
73
+ if (filterMode === 'api' && onSearch)
74
+ onSearch('');
75
+ inputRef.current?.focus();
76
+ };
77
+ const toggleDropdown = () => {
78
+ setIsOpen((prev) => {
79
+ if (!prev)
80
+ inputRef.current?.focus();
81
+ return !prev;
82
+ });
83
+ };
84
+ const visibleCount = Math.min(filteredData.length, visibleItems);
85
+ const dropdownHeight = visibleCount * heightItemC;
86
+ const getItemLayout = (_, index) => ({
87
+ length: heightItemC,
88
+ offset: heightItemC * index,
89
+ index,
90
+ });
91
+ const renderItem = ({ item, index }) => {
92
+ const isLast = index === filteredData.length - 1;
93
+ return (<react_native_1.TouchableOpacity style={[{ ...styles.item, height: heightItemC }, isLast && styles.itemLast]} onPress={() => handleSelect(item)} activeOpacity={0.6}>
94
+ <react_native_1.Text style={styles.itemText} numberOfLines={2}>
95
+ {String(item[filterKey])}
96
+ </react_native_1.Text>
97
+ <react_native_1.Text style={styles.itemChevron}>›</react_native_1.Text>
98
+ </react_native_1.TouchableOpacity>);
99
+ };
100
+ const showDropdown = isOpen && filteredData.length > 0;
101
+ const showEmpty = isOpen && filteredData.length === 0 && !loading;
102
+ const inputBorderColor = error
103
+ ? COLORS.danger
104
+ : isOpen
105
+ ? COLORS.primary
106
+ : COLORS.border;
107
+ return (<react_native_1.View style={[styles.wrapper, containerStyle]}>
108
+ {label && (<react_native_1.Text style={[styles.label, error && styles.labelError]}>
109
+ {label}
110
+ </react_native_1.Text>)}
111
+ <react_native_1.View style={[
112
+ styles.inputRow,
113
+ { borderColor: inputBorderColor },
114
+ isOpen && styles.inputRowOpen,
115
+ error && styles.inputRowError,
116
+ ]}>
117
+ <react_native_1.View style={styles.searchIconWrap}>
118
+ <react_native_1.Text style={[styles.searchIcon, isOpen && styles.searchIconActive]}>
119
+
120
+ </react_native_1.Text>
121
+ </react_native_1.View>
122
+
123
+ <react_native_1.TextInput ref={inputRef} style={styles.input} value={inputValue} onChangeText={handleChangeText} placeholder={placeholder} placeholderTextColor={COLORS.placeholder} onFocus={() => setIsOpen(true)} returnKeyType="done" onSubmitEditing={() => setIsOpen(false)}/>
124
+ <react_native_1.View style={styles.actions}>
125
+ {loading ? (<react_native_1.ActivityIndicator size="small" color={COLORS.primary}/>) : inputValue.length > 0 ? (<react_native_1.TouchableOpacity style={styles.iconBtn} onPress={handleClear} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
126
+ <react_native_1.View style={styles.clearBadge}>
127
+ <react_native_1.Text style={styles.clearIcon}>✕</react_native_1.Text>
128
+ </react_native_1.View>
129
+ </react_native_1.TouchableOpacity>) : (<react_native_1.TouchableOpacity style={styles.iconBtn} onPress={toggleDropdown} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
130
+ <react_native_1.Text style={[styles.chevron, isOpen && styles.chevronUp]}>
131
+
132
+ </react_native_1.Text>
133
+ </react_native_1.TouchableOpacity>)}
134
+ </react_native_1.View>
135
+ </react_native_1.View>
136
+ {helperText && (<react_native_1.Text style={[styles.helper, error && styles.helperError]}>
137
+ {helperText}
138
+ </react_native_1.Text>)}
139
+ {filterMode === 'api' && isOpen && (<react_native_1.View style={styles.modeBadge}>
140
+ <react_native_1.Text style={styles.modeBadgeText}>
141
+ {loading ? 'Buscando…' : `${filteredData.length} resultados`}
142
+ </react_native_1.Text>
143
+ </react_native_1.View>)}
144
+ {showDropdown && (<react_native_1.View style={[styles.dropdown, { height: dropdownHeight }]}>
145
+ <react_native_1.FlatList data={filteredData} keyExtractor={(_, i) => i.toString()} renderItem={renderItem} getItemLayout={getItemLayout} keyboardShouldPersistTaps="always" showsVerticalScrollIndicator={false} bounces={false} nestedScrollEnabled={true} scrollEnabled={true}/>
146
+ </react_native_1.View>)}
147
+ {showEmpty && (<react_native_1.View style={styles.emptyBox}>
148
+ <react_native_1.Text style={styles.emptyEmoji}>◎</react_native_1.Text>
149
+ <react_native_1.Text style={styles.emptyText}>Sin resultados</react_native_1.Text>
150
+ {filterMode === 'api' && (<react_native_1.Text style={styles.emptyHint}>
151
+ Intenta con otras palabras
152
+ </react_native_1.Text>)}
153
+ </react_native_1.View>)}
154
+ </react_native_1.View>);
155
+ }
156
+ const COLORS = {
157
+ primary: '#2563EB',
158
+ primaryLight: '#EFF6FF',
159
+ primaryMid: '#BFDBFE',
160
+ surface: '#FFFFFF',
161
+ surfaceAlt: '#F8FAFC',
162
+ border: '#E2E8F0',
163
+ text: '#0F172A',
164
+ textMuted: '#64748B',
165
+ placeholder: '#94A3B8',
166
+ danger: '#EF4444',
167
+ dangerLight: '#FEF2F2',
168
+ };
169
+ const styles = react_native_1.StyleSheet.create({
170
+ wrapper: {
171
+ width: '100%',
172
+ marginBottom: 20,
173
+ },
174
+ label: {
175
+ fontSize: 12,
176
+ fontWeight: '700',
177
+ color: COLORS.textMuted,
178
+ marginBottom: 7,
179
+ letterSpacing: 0.6,
180
+ textTransform: 'uppercase',
181
+ },
182
+ labelError: {
183
+ color: COLORS.danger,
184
+ },
185
+ inputRow: {
186
+ flexDirection: 'row',
187
+ alignItems: 'center',
188
+ height: 54,
189
+ borderWidth: 1.5,
190
+ borderBottomWidth: 1.5,
191
+ borderRadius: 14,
192
+ backgroundColor: COLORS.surface,
193
+ paddingRight: 6,
194
+ shadowColor: '#0F172A',
195
+ shadowOffset: { width: 0, height: 4 },
196
+ shadowOpacity: 0.05,
197
+ shadowRadius: 10,
198
+ },
199
+ inputRowOpen: {
200
+ marginBottom: 8,
201
+ borderBottomLeftRadius: 14,
202
+ borderBottomRightRadius: 14,
203
+ borderBottomWidth: 1.5,
204
+ },
205
+ inputRowError: {
206
+ borderColor: COLORS.danger,
207
+ backgroundColor: COLORS.dangerLight,
208
+ },
209
+ searchIconWrap: {
210
+ paddingLeft: 14,
211
+ paddingRight: 4,
212
+ justifyContent: 'center',
213
+ alignItems: 'center',
214
+ },
215
+ searchIcon: {
216
+ fontSize: 20,
217
+ color: COLORS.placeholder,
218
+ lineHeight: 24,
219
+ },
220
+ searchIconActive: {
221
+ color: COLORS.primary,
222
+ },
223
+ input: {
224
+ flex: 1,
225
+ height: '100%',
226
+ paddingHorizontal: 8,
227
+ fontSize: 15,
228
+ color: COLORS.text,
229
+ fontWeight: '400',
230
+ },
231
+ actions: {
232
+ paddingHorizontal: 6,
233
+ justifyContent: 'center',
234
+ alignItems: 'center',
235
+ minWidth: 36,
236
+ },
237
+ iconBtn: {
238
+ justifyContent: 'center',
239
+ alignItems: 'center',
240
+ },
241
+ clearBadge: {
242
+ width: 22,
243
+ height: 22,
244
+ borderRadius: 11,
245
+ // backgroundColor: COLORS.border,
246
+ justifyContent: 'center',
247
+ alignItems: 'center',
248
+ },
249
+ clearIcon: {
250
+ fontSize: 10,
251
+ color: COLORS.textMuted,
252
+ fontWeight: '700',
253
+ },
254
+ chevron: {
255
+ fontSize: 20,
256
+ color: COLORS.placeholder,
257
+ lineHeight: 24,
258
+ },
259
+ chevronUp: {
260
+ transform: [{ rotate: '180deg' }],
261
+ color: COLORS.primary,
262
+ },
263
+ helper: {
264
+ fontSize: 12,
265
+ color: COLORS.textMuted,
266
+ marginTop: 5,
267
+ marginLeft: 4,
268
+ },
269
+ helperError: {
270
+ color: COLORS.danger,
271
+ },
272
+ modeBadge: {
273
+ alignSelf: 'flex-end',
274
+ marginTop: 3,
275
+ marginRight: 2,
276
+ marginBottom: 4,
277
+ },
278
+ modeBadgeText: {
279
+ fontSize: 11,
280
+ color: COLORS.primary,
281
+ fontWeight: '600',
282
+ letterSpacing: 0.2,
283
+ },
284
+ dropdown: {
285
+ overflow: 'hidden',
286
+ borderWidth: 1.5,
287
+ borderRadius: 14,
288
+ borderColor: COLORS.border,
289
+ backgroundColor: COLORS.surface,
290
+ shadowColor: '#0F172A',
291
+ shadowOffset: { width: 0, height: 10 },
292
+ shadowOpacity: 0.08,
293
+ shadowRadius: 16,
294
+ elevation: 5,
295
+ },
296
+ item: {
297
+ flexDirection: 'row',
298
+ alignItems: 'center',
299
+ paddingHorizontal: 16,
300
+ borderBottomWidth: react_native_1.StyleSheet.hairlineWidth,
301
+ borderBottomColor: COLORS.border,
302
+ },
303
+ itemLast: {
304
+ borderBottomWidth: 0,
305
+ },
306
+ itemText: {
307
+ flex: 1,
308
+ fontSize: 14,
309
+ color: COLORS.text,
310
+ lineHeight: 20,
311
+ },
312
+ itemChevron: {
313
+ fontSize: 18,
314
+ color: COLORS.primaryMid,
315
+ fontWeight: '300',
316
+ marginLeft: 6,
317
+ },
318
+ emptyBox: {
319
+ paddingVertical: 24,
320
+ alignItems: 'center',
321
+ borderWidth: 1.5,
322
+ borderRadius: 14,
323
+ borderColor: COLORS.border,
324
+ backgroundColor: COLORS.surfaceAlt,
325
+ gap: 4,
326
+ marginTop: 8,
327
+ shadowColor: '#0F172A',
328
+ shadowOffset: { width: 0, height: 6 },
329
+ shadowOpacity: 0.04,
330
+ shadowRadius: 10,
331
+ elevation: 2,
332
+ },
333
+ emptyEmoji: {
334
+ fontSize: 22,
335
+ color: COLORS.placeholder,
336
+ },
337
+ emptyText: {
338
+ fontSize: 14,
339
+ color: COLORS.textMuted,
340
+ fontWeight: '600',
341
+ },
342
+ emptyHint: {
343
+ fontSize: 12,
344
+ color: COLORS.placeholder,
345
+ },
346
+ });
@@ -0,0 +1 @@
1
+ export { default as CustomAutocomplete } from "./components/CustomAutocomplete";
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CustomAutocomplete = void 0;
7
+ var CustomAutocomplete_1 = require("./components/CustomAutocomplete");
8
+ Object.defineProperty(exports, "CustomAutocomplete", { enumerable: true, get: function () { return __importDefault(CustomAutocomplete_1).default; } });
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@nova-ui-native/core",
3
+ "version": "1.0.0",
4
+ "description": "Una colección de componentes modernos, estilizados y profesionales para React Native",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "keywords": [
8
+ "react-native",
9
+ "components",
10
+ "ui-kit",
11
+ "autocomplete",
12
+ "design-system"
13
+ ],
14
+ "author": "Rafael Vides",
15
+ "license": "MIT",
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "prepare": "npm run build"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md"
23
+ ],
24
+ "peerDependencies": {
25
+ "react": "^19.2.7",
26
+ "react-native": "^0.86.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/react": "^19.2.17",
30
+ "typescript": "^5.9.3"
31
+ }
32
+ }