@controleonline/ui-default 1.0.263

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 (56) hide show
  1. package/.github/agents/developer.agent.md +30 -0
  2. package/.github/agents/devops.agent.md +30 -0
  3. package/.github/agents/qa.agent.md +30 -0
  4. package/.github/agents/security.agent.md +30 -0
  5. package/.scrutinizer.yml +20 -0
  6. package/AGENTS.md +47 -0
  7. package/FUNDING.yml +1 -0
  8. package/package.json +21 -0
  9. package/src/react/components/errors/DefaultErrors.js +360 -0
  10. package/src/react/components/files/DefaultFile.js +46 -0
  11. package/src/react/components/filters/CompactFilterSelector.js +262 -0
  12. package/src/react/components/filters/CompactFilterSelector.styles.js +124 -0
  13. package/src/react/components/filters/DateShortcutFilter.js +264 -0
  14. package/src/react/components/filters/DateShortcutFilter.styles.js +82 -0
  15. package/src/react/components/filters/DefaultColumnFilter.js +97 -0
  16. package/src/react/components/filters/DefaultColumnFilter.styles.js +21 -0
  17. package/src/react/components/filters/DefaultExternalFilters.js +441 -0
  18. package/src/react/components/filters/DefaultExternalFilters.styles.js +177 -0
  19. package/src/react/components/filters/DefaultSearch.js +103 -0
  20. package/src/react/components/filters/DefaultSearch.styles.js +70 -0
  21. package/src/react/components/filters/dateFilterSelection.js +29 -0
  22. package/src/react/components/form/DefaultForm.js +198 -0
  23. package/src/react/components/form/DefaultForm.styles.js +70 -0
  24. package/src/react/components/help/DefaultTooltip.js +87 -0
  25. package/src/react/components/help/DefaultTooltip.styles.js +61 -0
  26. package/src/react/components/inputs/DefaultInput.js +160 -0
  27. package/src/react/components/inputs/DefaultInput.styles.js +93 -0
  28. package/src/react/components/inputs/DefaultSelect.js +192 -0
  29. package/src/react/components/inputs/DefaultSelect.styles.js +65 -0
  30. package/src/react/components/inputs/defaultInputUtils.js +230 -0
  31. package/src/react/components/map/DefaultGoogleMap.styles.js +9 -0
  32. package/src/react/components/map/DefaultGoogleMap.web.js +698 -0
  33. package/src/react/components/map/DefaultMap.native.js +71 -0
  34. package/src/react/components/map/DefaultMap.shared.js +762 -0
  35. package/src/react/components/map/DefaultMap.styles.js +16 -0
  36. package/src/react/components/map/DefaultMap.web.js +66 -0
  37. package/src/react/components/map/DefaultNativeMap.native.js +615 -0
  38. package/src/react/components/map/DefaultNativeMap.shared.js +532 -0
  39. package/src/react/components/map/DefaultNativeMap.styles.js +122 -0
  40. package/src/react/components/table/DefaultTable.js +1897 -0
  41. package/src/react/components/table/DefaultTable.styles.js +610 -0
  42. package/src/react/index.js +10 -0
  43. package/src/react/utils/tableVisibleColumnsPreferences.js +264 -0
  44. package/src/store/default/actions.js +444 -0
  45. package/src/store/default/getters.js +28 -0
  46. package/src/store/default/mutation_types.js +26 -0
  47. package/src/store/default/mutations.js +138 -0
  48. package/src/tests/react/components/DateShortcutFilter.test.js +96 -0
  49. package/src/tests/react/components/errors/DefaultErrors.test.js +162 -0
  50. package/src/tests/react/components/files/DefaultFile.test.js +80 -0
  51. package/src/tests/react/components/map/DefaultMap.shared.test.js +162 -0
  52. package/src/tests/react/filters/dateFilterSelection.test.js +46 -0
  53. package/src/tests/react/inputs/defaultInputUtils.test.js +45 -0
  54. package/src/tests/react/store/defaultActions.test.js +112 -0
  55. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +223 -0
  56. package/src/utils/filters.js +56 -0
@@ -0,0 +1,82 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ const createStyles = themeColors =>
4
+ StyleSheet.create({
5
+ customLabel: {
6
+ color: themeColors.textPrimary,
7
+ fontSize: 12,
8
+ lineHeight: 14,
9
+ fontWeight: '900',
10
+ textTransform: 'uppercase',
11
+ letterSpacing: 0.7,
12
+ },
13
+ currentDateValue: {
14
+ color: themeColors.textPrimary,
15
+ fontSize: 12,
16
+ lineHeight: 14,
17
+ fontWeight: '800',
18
+ },
19
+ customRangeWrap: {
20
+ marginTop: 4,
21
+ paddingTop: 10,
22
+ borderTopWidth: 1,
23
+ borderTopColor: themeColors.borderSoft,
24
+ gap: 10,
25
+ },
26
+ customInputsRow: {
27
+ flexDirection: 'row',
28
+ gap: 8,
29
+ },
30
+ customInput: {
31
+ flex: 1,
32
+ borderRadius: 12,
33
+ borderWidth: 1,
34
+ borderColor: themeColors.border,
35
+ backgroundColor: themeColors.cardBgSoft,
36
+ color: themeColors.textPrimary,
37
+ paddingHorizontal: 12,
38
+ paddingVertical: 10,
39
+ fontSize: 13,
40
+ lineHeight: 16,
41
+ fontWeight: '700',
42
+ },
43
+ validationText: {
44
+ color: themeColors.danger,
45
+ fontSize: 12,
46
+ lineHeight: 14,
47
+ fontWeight: '700',
48
+ },
49
+ customActionsRow: {
50
+ flexDirection: 'row',
51
+ justifyContent: 'flex-end',
52
+ gap: 8,
53
+ },
54
+ secondaryButton: {
55
+ borderRadius: 999,
56
+ borderWidth: 1,
57
+ borderColor: themeColors.border,
58
+ backgroundColor: themeColors.panelBg,
59
+ paddingHorizontal: 14,
60
+ paddingVertical: 9,
61
+ },
62
+ secondaryButtonText: {
63
+ color: themeColors.textPrimary,
64
+ fontSize: 12,
65
+ lineHeight: 14,
66
+ fontWeight: '800',
67
+ },
68
+ primaryButton: {
69
+ borderRadius: 999,
70
+ backgroundColor: themeColors.accent,
71
+ paddingHorizontal: 14,
72
+ paddingVertical: 9,
73
+ },
74
+ primaryButtonText: {
75
+ color: themeColors.pillTextDark,
76
+ fontSize: 12,
77
+ lineHeight: 14,
78
+ fontWeight: '900',
79
+ },
80
+ });
81
+
82
+ export default createStyles;
@@ -0,0 +1,97 @@
1
+ import React from 'react';
2
+ import { TextInput, View } from 'react-native';
3
+ import {
4
+ buildOptionsFromColumn,
5
+ getColumnKey,
6
+ normalizeOptionKey,
7
+ normalizeText,
8
+ } from '../inputs/defaultInputUtils';
9
+ import CompactFilterSelector from './CompactFilterSelector';
10
+ import DateShortcutFilter from './DateShortcutFilter';
11
+ import { resolveNextDateFilterValue } from './dateFilterSelection';
12
+ import styles from './DefaultColumnFilter.styles';
13
+
14
+ const DefaultColumnFilter = ({
15
+ accentColor = '#2563EB',
16
+ column,
17
+ filters = {},
18
+ getOptionsForColumn = null,
19
+ onChange = null,
20
+ storeName = '',
21
+ style = null,
22
+ }) => {
23
+ const fieldName = getColumnKey(column);
24
+
25
+ if (column?.filter === false || column?.filters === false) {
26
+ return <View style={style} />;
27
+ }
28
+
29
+ if (column?.inputType === 'date-range' || column?.type === 'range-date') {
30
+ const filterValue = filters?.[fieldName] || {};
31
+ return (
32
+ <View style={[style, styles.filterCell]}>
33
+ <DateShortcutFilter
34
+ dense
35
+ store={storeName}
36
+ field={fieldName}
37
+ value={filterValue.shortcut || 'all'}
38
+ customRange={filterValue.customRange || { from: '', to: '' }}
39
+ onChange={optionKey =>
40
+ onChange?.(
41
+ fieldName,
42
+ resolveNextDateFilterValue(filterValue, optionKey),
43
+ )
44
+ }
45
+ onCustomRangeChange={range => onChange?.(fieldName, {
46
+ ...(filterValue || {}),
47
+ shortcut: 'custom',
48
+ customRange: range,
49
+ })}
50
+ />
51
+ </View>
52
+ );
53
+ }
54
+
55
+ if (column?.list) {
56
+ const rawOptions = buildOptionsFromColumn(column, getOptionsForColumn, storeName);
57
+ const options = [
58
+ { key: '', label: global.t?.t(storeName, 'label', 'select') || 'Selecionar' },
59
+ ...rawOptions,
60
+ ];
61
+ const selectedKey = normalizeOptionKey(filters?.[fieldName]);
62
+ const selected = options.find(option => option.key === selectedKey);
63
+
64
+ return (
65
+ <View style={[style, styles.filterCell]}>
66
+ <CompactFilterSelector
67
+ dense
68
+ store={storeName}
69
+ field={fieldName}
70
+ icon="filter"
71
+ accentColor={accentColor}
72
+ active={Boolean(selectedKey)}
73
+ label={selected?.label || options[0]?.label || ''}
74
+ options={options}
75
+ selectedKey={selectedKey}
76
+ onSelect={optionKey => {
77
+ onChange?.(fieldName, optionKey);
78
+ return true;
79
+ }}
80
+ />
81
+ </View>
82
+ );
83
+ }
84
+
85
+ return (
86
+ <View style={[style, styles.filterCell]}>
87
+ <TextInput
88
+ style={styles.filterInput}
89
+ value={normalizeText(filters?.[fieldName])}
90
+ placeholder={global.t?.t(storeName, 'input', column?.label || fieldName)}
91
+ onChangeText={value => onChange?.(fieldName, value)}
92
+ />
93
+ </View>
94
+ );
95
+ };
96
+
97
+ export default DefaultColumnFilter;
@@ -0,0 +1,21 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ const styles = StyleSheet.create({
4
+ filterCell: {
5
+ paddingVertical: 4,
6
+ backgroundColor: '#FFFFFF',
7
+ },
8
+ filterInput: {
9
+ height: 30,
10
+ borderWidth: 1,
11
+ borderColor: '#CBD5E1',
12
+ borderRadius: 8,
13
+ paddingHorizontal: 8,
14
+ color: '#0F172A',
15
+ backgroundColor: '#FFFFFF',
16
+ fontSize: 11,
17
+ fontWeight: '700',
18
+ },
19
+ });
20
+
21
+ export default styles;
@@ -0,0 +1,441 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import {
3
+ Modal,
4
+ ScrollView,
5
+ Text,
6
+ TextInput,
7
+ TouchableOpacity,
8
+ TouchableWithoutFeedback,
9
+ View,
10
+ useWindowDimensions,
11
+ } from 'react-native';
12
+ import Icon from 'react-native-vector-icons/Feather';
13
+ import { useStore } from '@store';
14
+ import { resolveThemePalette, withOpacity } from '@controleonline/../../src/styles/branding';
15
+ import { colors } from '@controleonline/../../src/styles/colors';
16
+ import CompactFilterSelector from './CompactFilterSelector';
17
+ import DateShortcutFilter from './DateShortcutFilter';
18
+ import { resolveNextDateFilterValue } from './dateFilterSelection';
19
+ import styles from './DefaultExternalFilters.styles';
20
+
21
+ const DEFAULT_COMPACT_BREAKPOINT = 768;
22
+ const noop = () => {};
23
+
24
+ const normalizeText = value => String(value || '').trim();
25
+ const DEFAULT_TRANSLATABLE_FIELDS = new Set([
26
+ 'active',
27
+ 'app',
28
+ 'channel',
29
+ 'displaytype',
30
+ 'featured',
31
+ 'frequency',
32
+ 'invoicetype',
33
+ 'installments',
34
+ 'ordertype',
35
+ 'peopletype',
36
+ 'pricecalculation',
37
+ 'productcondition',
38
+ 'realstatus',
39
+ 'status',
40
+ ]);
41
+
42
+ const getColumnKey = column => column?.key || column?.name || '';
43
+
44
+ const normalizeColumnKey = value =>
45
+ normalizeText(value)
46
+ .replace(/[^a-zA-Z0-9]/g, '')
47
+ .toLowerCase();
48
+
49
+ const shouldTranslateOptionLabel = (column, storeName, rawLabel) => {
50
+ if (column?.translate === false || !normalizeText(rawLabel) || !normalizeText(storeName)) {
51
+ return false;
52
+ }
53
+
54
+ if (column?.translate === true || Array.isArray(column?.list)) {
55
+ return true;
56
+ }
57
+
58
+ return [column?.key, column?.name, column?.label]
59
+ .map(normalizeColumnKey)
60
+ .filter(Boolean)
61
+ .some(candidate => DEFAULT_TRANSLATABLE_FIELDS.has(candidate));
62
+ };
63
+
64
+ const shouldIncludeColumn = column =>
65
+ Boolean(getColumnKey(column)) &&
66
+ column?.show !== false &&
67
+ column?.visible !== false &&
68
+ column?.externalFilter === true &&
69
+ column?.filter !== false &&
70
+ column?.filters !== false;
71
+
72
+ const normalizeFilterValue = value => {
73
+ if (value && typeof value === 'object') {
74
+ return normalizeFilterValue(value.value ?? value.id ?? value['@id'] ?? '');
75
+ }
76
+
77
+ return normalizeText(value);
78
+ };
79
+
80
+ const isFilledFilterValue = value => {
81
+ if (Array.isArray(value)) return value.length > 0;
82
+ if (value && typeof value === 'object') {
83
+ return Object.values(value).some(isFilledFilterValue);
84
+ }
85
+
86
+ return normalizeText(value) !== '';
87
+ };
88
+
89
+ const resolveOptionLabel = (column, option, storeName = '') => {
90
+ if (!option) return '';
91
+
92
+ const translateOptionLabel = rawLabel => {
93
+ const normalizedLabel = normalizeText(rawLabel);
94
+ if (!shouldTranslateOptionLabel(column, storeName, normalizedLabel)) {
95
+ return normalizedLabel;
96
+ }
97
+
98
+ return global.t?.t(storeName, 'label', normalizedLabel) || normalizedLabel;
99
+ };
100
+
101
+ if (typeof column?.formatList === 'function') {
102
+ const formatted = column.formatList(option, null, column);
103
+ if (formatted && typeof formatted === 'object') {
104
+ return translateOptionLabel(formatted.label ?? formatted.value);
105
+ }
106
+ if (formatted) return translateOptionLabel(formatted);
107
+ }
108
+
109
+ const rawLabel = normalizeText(
110
+ option.label ??
111
+ option[column?.searchParam] ??
112
+ option[column?.name] ??
113
+ option.name ??
114
+ option.status ??
115
+ option.wallet ??
116
+ option.paymentType ??
117
+ option.alias ??
118
+ option.id,
119
+ );
120
+
121
+ return translateOptionLabel(rawLabel);
122
+ };
123
+
124
+ const buildColumnOptions = (column, options = [], storeName = '') => [
125
+ {
126
+ key: '',
127
+ label: global.t?.t(storeName || 'invoice', 'label', 'select'),
128
+ },
129
+ ...(Array.isArray(options) ? options : []).map(option => ({
130
+ key: normalizeFilterValue(option),
131
+ label: resolveOptionLabel(column, option, storeName) || '-',
132
+ })),
133
+ ];
134
+
135
+ const resolveDateState = filterValue => {
136
+ if (!filterValue || typeof filterValue !== 'object') {
137
+ return {
138
+ customRange: { from: '', to: '' },
139
+ value: 'all',
140
+ };
141
+ }
142
+
143
+ if (filterValue.shortcut) {
144
+ return {
145
+ customRange: filterValue.customRange || { from: '', to: '' },
146
+ value: filterValue.shortcut,
147
+ };
148
+ }
149
+
150
+ if (filterValue.start || filterValue.end || filterValue.after || filterValue.before) {
151
+ return {
152
+ customRange: {
153
+ from: filterValue.start || filterValue.after || '',
154
+ to: filterValue.end || filterValue.before || '',
155
+ },
156
+ value: 'custom',
157
+ };
158
+ }
159
+
160
+ return {
161
+ customRange: { from: '', to: '' },
162
+ value: 'all',
163
+ };
164
+ };
165
+
166
+ const resolveIcon = column => {
167
+ const key = getColumnKey(column);
168
+ if (key === 'status') return 'check-circle';
169
+ if (key === 'category') return 'tag';
170
+ if (column?.inputType === 'date-range' || column?.type === 'range-date') return 'calendar';
171
+ return 'sliders';
172
+ };
173
+
174
+ const DefaultExternalFilters = ({
175
+ accentColor = null,
176
+ compactBreakpoint = DEFAULT_COMPACT_BREAKPOINT,
177
+ columns = [],
178
+ dateOptionKeys = ['all', 'today', 'yesterday', '7d', '30d', 'custom'],
179
+ filters = {},
180
+ getOptionsForColumn = null,
181
+ onActiveCountChange = null,
182
+ onChangeFilters = null,
183
+ storeName = '',
184
+ }) => {
185
+ const { width } = useWindowDimensions();
186
+ const peopleStore = useStore('people');
187
+ const themeStore = useStore('theme');
188
+ const [isFiltersModalOpen, setIsFiltersModalOpen] = useState(false);
189
+ const { currentCompany } = peopleStore.getters || {};
190
+ const { colors: themeColors } = themeStore.getters || {};
191
+ const themeTokens = useMemo(
192
+ () => ({...themeColors, ...(currentCompany?.theme?.colors || {})}),
193
+ [currentCompany?.theme?.colors, themeColors],
194
+ );
195
+ const palette = useMemo(
196
+ () => resolveThemePalette(themeTokens, colors),
197
+ [themeTokens],
198
+ );
199
+ const resolvedAccentColor = accentColor || palette.primary;
200
+ const surfaceColor = themeTokens['bg-odd-light'] || palette.background;
201
+ const panelColor = themeTokens['bg-headers-light'] || palette.background;
202
+ const borderColor = palette.border;
203
+ const textColor = palette.text;
204
+ const textSecondaryColor = palette.textSecondary;
205
+ const onAccentColor = palette.secondary || palette.text;
206
+ const isCompactView = width > 0 && width <= compactBreakpoint;
207
+ const filterColumns = useMemo(
208
+ () => columns.filter(shouldIncludeColumn),
209
+ [columns],
210
+ );
211
+ const activeCount = useMemo(
212
+ () => filterColumns.filter(column => isFilledFilterValue(filters[getColumnKey(column)])).length,
213
+ [filterColumns, filters],
214
+ );
215
+
216
+ useEffect(() => {
217
+ onActiveCountChange?.(activeCount);
218
+ }, [activeCount, onActiveCountChange]);
219
+
220
+ const updateFilter = useCallback((key, value) => {
221
+ const nextFilters = { ...(filters || {}) };
222
+ if (!isFilledFilterValue(value)) {
223
+ delete nextFilters[key];
224
+ } else {
225
+ nextFilters[key] = value;
226
+ }
227
+ onChangeFilters?.(nextFilters);
228
+ }, [filters, onChangeFilters]);
229
+
230
+ const clearFilters = useCallback(() => {
231
+ onChangeFilters?.({});
232
+ }, [onChangeFilters]);
233
+
234
+ useEffect(() => {
235
+ if (!isCompactView) setIsFiltersModalOpen(false);
236
+ }, [isCompactView]);
237
+
238
+ if (filterColumns.length === 0) return null;
239
+
240
+ const renderFilterField = (column, compact = false) => {
241
+ const key = getColumnKey(column);
242
+ const fieldStyle = compact ? styles.modalField : styles.field;
243
+
244
+ if (column.inputType === 'date-range' || column.type === 'range-date') {
245
+ const dateState = resolveDateState(filters[key]);
246
+
247
+ return (
248
+ <View key={key} style={fieldStyle}>
249
+ <DateShortcutFilter
250
+ value={dateState.value}
251
+ onChange={optionKey => {
252
+ updateFilter(
253
+ key,
254
+ resolveNextDateFilterValue(filters[key], optionKey),
255
+ );
256
+ }}
257
+ customRange={dateState.customRange}
258
+ onCustomRangeChange={range => {
259
+ updateFilter(key, {
260
+ ...(filters[key] || {}),
261
+ shortcut: 'custom',
262
+ customRange: range,
263
+ });
264
+ }}
265
+ dense
266
+ store={storeName}
267
+ field={key}
268
+ optionKeys={dateOptionKeys}
269
+ colors={{
270
+ accent: resolvedAccentColor,
271
+ appBg: 'transparent',
272
+ border: borderColor,
273
+ borderSoft: withOpacity(borderColor, 0.72),
274
+ cardBg: palette.background,
275
+ cardBgSoft: surfaceColor,
276
+ danger: palette.error,
277
+ isLight: true,
278
+ panelBg: panelColor,
279
+ pillTextDark: onAccentColor,
280
+ textPrimary: textColor,
281
+ textSecondary: textSecondaryColor,
282
+ }}
283
+ />
284
+ </View>
285
+ );
286
+ }
287
+
288
+ if (column.list) {
289
+ const options = buildColumnOptions(column, getOptionsForColumn?.(column) || [], storeName);
290
+ const selectedKey = normalizeFilterValue(filters[key]);
291
+ const selectedLabel =
292
+ options.find(option => option.key === selectedKey)?.label ||
293
+ options[0]?.label ||
294
+ '';
295
+
296
+ return (
297
+ <View key={key} style={fieldStyle}>
298
+ <CompactFilterSelector
299
+ icon={resolveIcon(column)}
300
+ label={selectedLabel}
301
+ accentColor={resolvedAccentColor}
302
+ active={Boolean(selectedKey)}
303
+ dense
304
+ store={storeName}
305
+ field={key}
306
+ options={options}
307
+ selectedKey={selectedKey}
308
+ onSelect={optionKey => {
309
+ updateFilter(key, optionKey);
310
+ return true;
311
+ }}
312
+ />
313
+ </View>
314
+ );
315
+ }
316
+
317
+ return (
318
+ <View key={key} style={[fieldStyle, styles.inputWrap, { borderColor, backgroundColor: surfaceColor }]}>
319
+ <Text style={[styles.inputLabel, { color: textSecondaryColor }]}>
320
+ {global.t?.t(storeName, 'input', column.label || key)}
321
+ </Text>
322
+ <TextInput
323
+ style={[styles.input, { color: textColor }]}
324
+ value={normalizeText(filters[key])}
325
+ onChangeText={value => updateFilter(key, value)}
326
+ onSubmitEditing={() => onChangeFilters?.(filters)}
327
+ />
328
+ </View>
329
+ );
330
+ };
331
+
332
+ const filterTitle = global.t?.t(storeName, 'label', 'filters');
333
+ const filterFields = compact => filterColumns.map(column => renderFilterField(column, compact));
334
+
335
+ if (isCompactView) {
336
+ return (
337
+ <View style={styles.mobileWrap}>
338
+ <TouchableOpacity
339
+ style={[
340
+ styles.mobileButton,
341
+ { borderColor: borderColor, backgroundColor: surfaceColor },
342
+ activeCount > 0 ? { borderColor: resolvedAccentColor, backgroundColor: withOpacity(resolvedAccentColor, 0.14) } : null,
343
+ ]}
344
+ activeOpacity={0.84}
345
+ onPress={() => setIsFiltersModalOpen(true)}
346
+ >
347
+ <Icon name="filter" size={15} color={activeCount > 0 ? resolvedAccentColor : textSecondaryColor} />
348
+ <Text
349
+ style={[
350
+ styles.mobileButtonText,
351
+ { color: textColor },
352
+ activeCount > 0 ? { color: resolvedAccentColor } : null,
353
+ ]}
354
+ >
355
+ {filterTitle}
356
+ </Text>
357
+ {activeCount > 0 ? (
358
+ <View style={[styles.mobileCountBadge, { backgroundColor: resolvedAccentColor }]}>
359
+ <Text style={styles.mobileCountBadgeText}>{activeCount}</Text>
360
+ </View>
361
+ ) : null}
362
+ </TouchableOpacity>
363
+
364
+ <Modal
365
+ transparent
366
+ visible={isFiltersModalOpen}
367
+ animationType="fade"
368
+ onRequestClose={() => setIsFiltersModalOpen(false)}
369
+ >
370
+ <TouchableWithoutFeedback onPress={() => setIsFiltersModalOpen(false)}>
371
+ <View style={[styles.modalOverlay, { backgroundColor: withOpacity(textColor, 0.42) }]}>
372
+ <TouchableWithoutFeedback onPress={noop}>
373
+ <View style={[styles.modalCard, { borderColor, backgroundColor: surfaceColor }]}>
374
+ <View style={[styles.modalHeader, { borderBottomColor: borderColor }]}>
375
+ <Text style={[styles.modalTitle, { color: textColor }]}>{filterTitle}</Text>
376
+ <TouchableOpacity
377
+ style={[styles.modalCloseButton, { borderColor: borderColor, backgroundColor: surfaceColor }]}
378
+ activeOpacity={0.82}
379
+ onPress={() => setIsFiltersModalOpen(false)}
380
+ >
381
+ <Icon name="x" size={18} color={textSecondaryColor} />
382
+ </TouchableOpacity>
383
+ </View>
384
+
385
+ <ScrollView
386
+ style={styles.modalScroll}
387
+ contentContainerStyle={styles.modalContent}
388
+ keyboardShouldPersistTaps="handled"
389
+ >
390
+ {filterFields(true)}
391
+ </ScrollView>
392
+
393
+ <View style={[styles.modalActions, { borderTopColor: borderColor }]}>
394
+ {activeCount > 0 ? (
395
+ <TouchableOpacity
396
+ style={[styles.modalSecondaryButton, { borderColor: borderColor, backgroundColor: surfaceColor }]}
397
+ activeOpacity={0.84}
398
+ onPress={clearFilters}
399
+ >
400
+ <Text style={[styles.modalSecondaryButtonText, { color: textColor }]}>
401
+ {global.t?.t(storeName, 'button', 'clear')}
402
+ </Text>
403
+ </TouchableOpacity>
404
+ ) : null}
405
+ <TouchableOpacity
406
+ style={[styles.modalPrimaryButton, { backgroundColor: resolvedAccentColor }]}
407
+ activeOpacity={0.84}
408
+ onPress={() => setIsFiltersModalOpen(false)}
409
+ >
410
+ <Text style={[styles.modalPrimaryButtonText, { color: onAccentColor }]}>
411
+ {global.t?.t(storeName, 'button', 'apply')}
412
+ </Text>
413
+ </TouchableOpacity>
414
+ </View>
415
+ </View>
416
+ </TouchableWithoutFeedback>
417
+ </View>
418
+ </TouchableWithoutFeedback>
419
+ </Modal>
420
+ </View>
421
+ );
422
+ }
423
+
424
+ return (
425
+ <View style={styles.wrap}>
426
+ {filterFields(false)}
427
+
428
+ {activeCount > 0 ? (
429
+ <TouchableOpacity
430
+ style={[styles.clearButton, { borderColor: borderColor, backgroundColor: surfaceColor }]}
431
+ activeOpacity={0.82}
432
+ onPress={clearFilters}
433
+ >
434
+ <Icon name="x" size={14} color={textSecondaryColor} />
435
+ </TouchableOpacity>
436
+ ) : null}
437
+ </View>
438
+ );
439
+ };
440
+
441
+ export default DefaultExternalFilters;