@controleonline/ui-default 1.0.269 → 1.0.272

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 (91) hide show
  1. package/.github/workflows/integration-source-gate.yml +68 -0
  2. package/.github/workflows/manager-worker.yml +19 -0
  3. package/AGENTS.md +46 -50
  4. package/docs/technical/Contrato-de-colunas.md +67 -0
  5. package/docs/technical/Default-Components.md +76 -0
  6. package/docs/technical/DefaultForm.md +80 -0
  7. package/docs/technical/DefaultInput-e-DefaultSelect.md +73 -0
  8. package/docs/technical/DefaultTable.md +203 -0
  9. package/docs/wiki.md +1 -0
  10. package/package.json +22 -21
  11. package/scripts/validate-wiki-pointer.cjs +13 -0
  12. package/src/react/components/address/DefaultAddress.js +435 -0
  13. package/src/react/components/address/DefaultAddress.styles.js +133 -0
  14. package/src/react/components/address/DefaultAddressMapPane.js +55 -0
  15. package/src/react/components/address/LatLonReadonlyFields.js +51 -0
  16. package/src/react/components/address/defaultAddressHelpers.js +163 -0
  17. package/src/react/components/address/defaultAddressStyles.js +136 -0
  18. package/src/react/components/address/defaultAddressUtils.js +129 -0
  19. package/src/react/components/extra-data/DefaultExtraData.js +152 -0
  20. package/src/react/components/extra-data/DefaultExtraData.styles.js +61 -0
  21. package/src/react/components/files/DefaultFile.js +231 -46
  22. package/src/react/components/files/DefaultFileColumn.js +123 -0
  23. package/src/react/components/files/DefaultFileColumn.styles.js +52 -0
  24. package/src/react/components/files/DefaultFileColumn.utils.js +104 -0
  25. package/src/react/components/filters/CompactFilterSelector.js +51 -35
  26. package/src/react/components/filters/DefaultColumnFilter.js +58 -41
  27. package/src/react/components/filters/DefaultExternalFilters.js +331 -265
  28. package/src/react/components/form/DefaultForm.js +99 -79
  29. package/src/react/components/help/DefaultTooltip.js +59 -33
  30. package/src/react/components/help/DefaultTooltip.styles.js +23 -3
  31. package/src/react/components/inputs/DefaultColorInput.js +153 -0
  32. package/src/react/components/inputs/DefaultFeatherIconPicker.js +146 -0
  33. package/src/react/components/inputs/DefaultFeatherIconPicker.styles.js +113 -0
  34. package/src/react/components/inputs/DefaultInput.js +69 -5
  35. package/src/react/components/inputs/DefaultInput.styles.js +26 -0
  36. package/src/react/components/inputs/DefaultSelect.js +134 -123
  37. package/src/react/components/inputs/defaultInputUtils.js +69 -43
  38. package/src/react/components/map/DefaultGoogleMap.styles.js +9 -2
  39. package/src/react/components/map/DefaultGoogleMap.web.js +49 -28
  40. package/src/react/components/map/DefaultMap.native.js +73 -73
  41. package/src/react/components/map/DefaultMap.shared.js +911 -762
  42. package/src/react/components/map/DefaultMap.web.js +124 -68
  43. package/src/react/components/table/DefaultColumnMenu.js +109 -108
  44. package/src/react/components/table/DefaultDebug.js +45 -38
  45. package/src/react/components/table/DefaultSearchModal.js +9 -1
  46. package/src/react/components/table/DefaultTable.grouping.js +98 -0
  47. package/src/react/components/table/DefaultTable.js +215 -109
  48. package/src/react/components/table/DefaultTable.styles.js +367 -314
  49. package/src/react/components/table/DefaultTableBody.js +13 -8
  50. package/src/react/components/table/DefaultTableCards.js +20 -1
  51. package/src/react/components/table/DefaultTableControls.js +180 -205
  52. package/src/react/components/table/DefaultTableImportModal.js +108 -0
  53. package/src/react/components/table/DefaultTableInput.js +1 -0
  54. package/src/react/components/table/DefaultTableRowActions.js +1 -1
  55. package/src/react/components/table/DefaultTableRows.js +224 -363
  56. package/src/react/components/table/DefaultTableSearch.js +28 -5
  57. package/src/react/components/table/DefaultTableToolbar.js +92 -3
  58. package/src/react/components/table/useDefaultTablePagination.js +56 -7
  59. package/src/react/components/table/useDefaultTableStoreSync.js +164 -0
  60. package/src/react/components/upload/DefaultUpload.js +436 -0
  61. package/src/react/components/upload/DefaultUpload.styles.js +153 -0
  62. package/src/react/components/upload/DefaultUploadAttachmentsList.js +138 -0
  63. package/src/react/components/upload/DefaultUploadManagerModal.js +247 -0
  64. package/src/react/components/upload/defaultUploadHelpers.js +108 -0
  65. package/src/react/components/upload/defaultUploadLibrary.js +248 -0
  66. package/src/react/components/upload/fileUpload.js +93 -0
  67. package/src/react/hooks/usePostalCodeLookup.js +145 -0
  68. package/src/react/index.js +6 -0
  69. package/src/react/services/addressFormUtils.js +196 -0
  70. package/src/react/services/addressGeo.js +227 -0
  71. package/src/react/utils/tableVisibleColumnsPreferences.js +29 -12
  72. package/src/tests/react/components/address/defaultAddressHelpers.test.js +104 -0
  73. package/src/tests/react/components/files/DefaultFile.test.js +6 -6
  74. package/src/tests/react/components/form/DefaultForm.test.js +123 -0
  75. package/src/tests/react/components/inputs/DefaultSelect.test.js +108 -79
  76. package/src/tests/react/components/table/DefaultTable.grouping.test.js +49 -0
  77. package/src/tests/react/components/table/DefaultTable.test.js +54 -2
  78. package/src/tests/react/components/table/DefaultTableControls.filters.test.mjs +21 -0
  79. package/src/tests/react/components/table/resolveRowBackgroundColor.test.mjs +45 -0
  80. package/src/tests/react/components/table/useDefaultTableStoreSync.test.js +161 -0
  81. package/src/tests/react/components/upload/DefaultUpload.test.js +353 -0
  82. package/src/tests/react/components/upload/defaultUploadLibrary.test.js +82 -0
  83. package/src/tests/react/components/upload/peopleMediaLibraryPreview.test.mjs +57 -0
  84. package/src/tests/react/hooks/usePostalCodeLookup.test.js +40 -0
  85. package/src/tests/react/services/SMOKE-637-cep-hydrate.md +45 -0
  86. package/src/tests/react/services/addressFormUtils.test.js +176 -0
  87. package/src/tests/react/services/addressGeoLookup.async.test.js +104 -0
  88. package/src/tests/react/services/addressGeoLookup.test.js +105 -0
  89. package/src/tests/react/services/smoke-637/form-cep-12941040.png +0 -0
  90. package/src/tests/react/services/smoke-637/index.html +132 -0
  91. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +84 -48
@@ -0,0 +1,133 @@
1
+ import {StyleSheet} from 'react-native';
2
+
3
+ export default StyleSheet.create({
4
+ scroll: {flex: 1, width: '100%'},
5
+ container: {padding: 16, gap: 16},
6
+ containerDesktop: {
7
+ flexDirection: 'row',
8
+ alignItems: 'flex-start',
9
+ alignSelf: 'center',
10
+ width: '100%',
11
+ maxWidth: 1280,
12
+ paddingHorizontal: 32,
13
+ paddingVertical: 24,
14
+ gap: 24,
15
+ },
16
+ formPane: {width: '100%'},
17
+ formPaneDesktop: {flex: 1, maxWidth: 560},
18
+ field: {marginBottom: 10, position: 'relative', zIndex: 1},
19
+ fieldRaised: {zIndex: 30, elevation: 8},
20
+ label: {fontSize: 13, fontWeight: '600', marginBottom: 4, color: '#334155'},
21
+ input: {
22
+ borderWidth: 1,
23
+ borderColor: '#CBD5E1',
24
+ borderRadius: 8,
25
+ paddingHorizontal: 12,
26
+ paddingVertical: 10,
27
+ backgroundColor: '#fff',
28
+ },
29
+ select: {
30
+ borderWidth: 1,
31
+ borderColor: '#CBD5E1',
32
+ borderRadius: 8,
33
+ paddingHorizontal: 12,
34
+ paddingVertical: 12,
35
+ backgroundColor: '#F8FAFC',
36
+ },
37
+ dropdown: {
38
+ maxHeight: 180,
39
+ borderWidth: 1,
40
+ borderColor: '#E2E8F0',
41
+ borderRadius: 8,
42
+ marginTop: 4,
43
+ backgroundColor: '#fff',
44
+ overflow: 'hidden',
45
+ zIndex: 40,
46
+ elevation: 8,
47
+ },
48
+ option: {paddingHorizontal: 12, paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: '#F1F5F9'},
49
+ row: {flexDirection: 'row', alignItems: 'center'},
50
+ flex: {flex: 1},
51
+ loader: {marginLeft: 8},
52
+ error: {color: '#B91C1C', marginBottom: 8},
53
+ hint: {color: '#64748B', marginBottom: 8, fontSize: 12},
54
+ mapPane: {
55
+ width: '100%',
56
+ borderWidth: 1,
57
+ borderColor: '#E2E8F0',
58
+ borderRadius: 12,
59
+ backgroundColor: '#F8FAFC',
60
+ padding: 16,
61
+ gap: 12,
62
+ },
63
+ mapPaneDesktop: {flex: 1, minHeight: 560},
64
+ mapPaneTitle: {fontWeight: '700', fontSize: 15, color: '#0F172A'},
65
+ mapImage: {
66
+ width: '100%',
67
+ height: 220,
68
+ borderRadius: 10,
69
+ backgroundColor: '#E2E8F0',
70
+ },
71
+ mapImageDesktop: {height: 360},
72
+ liveMap: {
73
+ width: '100%',
74
+ height: 220,
75
+ borderRadius: 10,
76
+ overflow: 'hidden',
77
+ backgroundColor: '#E2E8F0',
78
+ },
79
+ liveMapDesktop: {height: 360},
80
+ facadeImage: {
81
+ width: '100%',
82
+ height: 180,
83
+ borderRadius: 10,
84
+ backgroundColor: '#E2E8F0',
85
+ },
86
+ mapPlaceholder: {
87
+ height: 220,
88
+ borderRadius: 10,
89
+ backgroundColor: '#E2E8F0',
90
+ alignItems: 'center',
91
+ justifyContent: 'center',
92
+ padding: 18,
93
+ },
94
+ facadePlaceholder: {
95
+ minHeight: 120,
96
+ borderRadius: 10,
97
+ backgroundColor: '#E2E8F0',
98
+ alignItems: 'center',
99
+ justifyContent: 'center',
100
+ padding: 18,
101
+ },
102
+ mapPlaceholderTitle: {
103
+ fontWeight: '700',
104
+ color: '#334155',
105
+ marginBottom: 6,
106
+ },
107
+ mapPlaceholderText: {
108
+ color: '#64748B',
109
+ textAlign: 'center',
110
+ fontSize: 13,
111
+ lineHeight: 18,
112
+ },
113
+ actions: {flexDirection: 'row', justifyContent: 'flex-end', gap: 12, marginTop: 16},
114
+ btnSecondary: {paddingHorizontal: 16, paddingVertical: 12},
115
+ btnPrimary: {
116
+ backgroundColor: '#1D4ED8',
117
+ paddingHorizontal: 18,
118
+ paddingVertical: 12,
119
+ borderRadius: 8,
120
+ },
121
+ btnPrimaryText: {color: '#fff', fontWeight: '600'},
122
+ coordRow: {
123
+ flexDirection: 'row',
124
+ gap: 12,
125
+ },
126
+ coordField: {
127
+ flex: 1,
128
+ },
129
+ inputReadonly: {
130
+ backgroundColor: '#F1F5F9',
131
+ color: '#475569',
132
+ },
133
+ });
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+ import {Image, Text, View} from 'react-native';
3
+ import DefaultMap from '../map/DefaultMap';
4
+ import {hasCoordinates} from './defaultAddressHelpers';
5
+ import {styles} from './defaultAddressStyles';
6
+
7
+ /**
8
+ * Map + facade preview pane for DefaultAddress.
9
+ */
10
+ export default function DefaultAddressMapPane({
11
+ form,
12
+ mapMarkerPayload,
13
+ mapUserCoordinates,
14
+ isDesktop,
15
+ }) {
16
+ return (
17
+ <View style={[styles.mapPane, isDesktop && styles.mapPaneDesktop]}>
18
+ <Text style={styles.mapPaneTitle}>Mapa</Text>
19
+ {hasCoordinates(form) || mapMarkerPayload ? (
20
+ <View style={[styles.liveMap, isDesktop && styles.liveMapDesktop]}>
21
+ <DefaultMap
22
+ markerPayloads={mapMarkerPayload ? [mapMarkerPayload] : []}
23
+ userCoordinates={mapUserCoordinates}
24
+ />
25
+ </View>
26
+ ) : form.mapStaticUrl ? (
27
+ <Image
28
+ source={{uri: form.mapStaticUrl}}
29
+ style={[styles.mapImage, isDesktop && styles.mapImageDesktop]}
30
+ resizeMode="cover"
31
+ />
32
+ ) : (
33
+ <View style={[styles.mapPlaceholder, isDesktop && styles.mapImageDesktop]}>
34
+ <Text style={styles.mapPlaceholderTitle}>Mapa indisponível</Text>
35
+ <Text style={styles.mapPlaceholderText}>
36
+ Consulte um CEP para carregar a localização quando a API retornar a
37
+ imagem, ou informe latitude/longitude manualmente.
38
+ </Text>
39
+ </View>
40
+ )}
41
+
42
+ {/* Fachada only when provider returned street-view URL (not OSM). */}
43
+ {form.facadeUrl ? (
44
+ <>
45
+ <Text style={styles.mapPaneTitle}>Fachada</Text>
46
+ <Image
47
+ source={{uri: form.facadeUrl}}
48
+ style={styles.facadeImage}
49
+ resizeMode="cover"
50
+ />
51
+ </>
52
+ ) : null}
53
+ </View>
54
+ );
55
+ }
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import {View, TextInput} from 'react-native';
3
+
4
+ /**
5
+ * Latitude / longitude fields.
6
+ * Read-only when both coordinates are present and the user has not started
7
+ * a manual edit; editable when coords are missing so the user can type them.
8
+ */
9
+ export default function LatLonReadonlyFields({
10
+ form,
11
+ styles,
12
+ Field,
13
+ editable = false,
14
+ onChange,
15
+ }) {
16
+ const formatCoord = value =>
17
+ value == null || value === '' ? '' : String(value);
18
+
19
+ return (
20
+ <View style={styles.coordRow}>
21
+ <View style={styles.coordField}>
22
+ <Field label="Latitude">
23
+ <TextInput
24
+ testID="address-latitude-input"
25
+ style={[styles.input, editable ? null : styles.inputReadonly]}
26
+ value={formatCoord(form?.latitude)}
27
+ editable={editable}
28
+ selectTextOnFocus={editable}
29
+ onChangeText={v => onChange?.('latitude', v)}
30
+ placeholder="—"
31
+ keyboardType="decimal-pad"
32
+ />
33
+ </Field>
34
+ </View>
35
+ <View style={styles.coordField}>
36
+ <Field label="Longitude">
37
+ <TextInput
38
+ testID="address-longitude-input"
39
+ style={[styles.input, editable ? null : styles.inputReadonly]}
40
+ value={formatCoord(form?.longitude)}
41
+ editable={editable}
42
+ selectTextOnFocus={editable}
43
+ onChangeText={v => onChange?.('longitude', v)}
44
+ placeholder="—"
45
+ keyboardType="decimal-pad"
46
+ />
47
+ </Field>
48
+ </View>
49
+ </View>
50
+ );
51
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Pure helpers for DefaultAddress — keep the component under the 500-line limit.
3
+ * @see app-community#283 (franchise address lat/long + manual fields)
4
+ */
5
+
6
+ export const emptyForm = {
7
+ nickname: '',
8
+ cep: '',
9
+ street: '',
10
+ number: '',
11
+ complement: '',
12
+ district: '',
13
+ city: '',
14
+ uf: '',
15
+ stateName: '',
16
+ countryCode: 'BR',
17
+ countryName: 'Brazil',
18
+ latitude: null,
19
+ longitude: null,
20
+ mapStaticUrl: null,
21
+ facadeUrl: null,
22
+ provider: null,
23
+ categories: [],
24
+ };
25
+
26
+ export const onlyDigits = value => String(value || '').replace(/\D+/g, '');
27
+
28
+ export const hydrateFromRow = row => {
29
+ if (!row || typeof row !== 'object') {
30
+ return {...emptyForm};
31
+ }
32
+ const street = row?.street?.street || row?.street || '';
33
+ const district = row?.street?.district?.district || row?.district || '';
34
+ const city = row?.street?.district?.city?.city || row?.city || '';
35
+ const stateEntity = row?.street?.district?.city?.state || row?.state;
36
+ const countryEntity = stateEntity?.country || row?.country;
37
+ return {
38
+ ...emptyForm,
39
+ nickname: row?.nickname || '',
40
+ cep: String(row?.street?.cep?.cep || row?.cep || row?.postal_code || ''),
41
+ street: typeof street === 'string' ? street : '',
42
+ number: row?.number != null ? String(row.number) : '',
43
+ complement: row?.complement || '',
44
+ district,
45
+ city,
46
+ uf: stateEntity?.uf || row?.uf || '',
47
+ stateName: stateEntity?.state || '',
48
+ countryCode: countryEntity?.countrycode || countryEntity?.code || 'BR',
49
+ countryName: countryEntity?.countryname || countryEntity?.name || 'Brazil',
50
+ latitude: row?.latitude ?? null,
51
+ longitude: row?.longitude ?? null,
52
+ categories: Array.isArray(row?.categories)
53
+ ? row.categories
54
+ : Array.isArray(row?.category)
55
+ ? row.category
56
+ : [],
57
+ };
58
+ };
59
+
60
+ export const hasAddressText = form =>
61
+ [form?.street, form?.number, form?.district, form?.city, form?.uf, form?.cep].some(
62
+ value => String(value || '').trim().length > 0,
63
+ );
64
+
65
+ export const hasCoordinates = form =>
66
+ Number.isFinite(Number(form?.latitude)) && Number.isFinite(Number(form?.longitude));
67
+
68
+ export const parseOptionalCoordinate = value => {
69
+ if (value === null || value === undefined || value === '') {
70
+ return null;
71
+ }
72
+ const n = Number(String(value).trim().replace(',', '.'));
73
+ return Number.isFinite(n) ? n : null;
74
+ };
75
+
76
+ export const mergePostalCodeData = (prev, data, {preserveFilledFields = false} = {}) => {
77
+ const keep = (key, nextValue) =>
78
+ preserveFilledFields && String(prev[key] || '').trim()
79
+ ? prev[key]
80
+ : nextValue || prev[key];
81
+
82
+ return {
83
+ ...prev,
84
+ cep: onlyDigits(data.cep || prev.cep),
85
+ street: keep('street', data.street),
86
+ district: keep('district', data.district),
87
+ city: keep('city', data.city),
88
+ uf: keep('uf', data.uf || data.state),
89
+ stateName: keep('stateName', data.state),
90
+ countryCode:
91
+ data.country === 'Brasil' || data.country === 'Brazil'
92
+ ? 'BR'
93
+ : data.country || prev.countryCode,
94
+ countryName:
95
+ data.country === 'Brasil' ? 'Brazil' : data.country || prev.countryName,
96
+ latitude: data.latitude ?? data.map?.latitude ?? prev.latitude,
97
+ longitude: data.longitude ?? data.map?.longitude ?? prev.longitude,
98
+ mapStaticUrl: data.map?.staticUrl || prev.mapStaticUrl || null,
99
+ facadeUrl: data.facade?.streetViewUrl || prev.facadeUrl || null,
100
+ provider: data.provider || prev.provider || null,
101
+ };
102
+ };
103
+
104
+ export const getCurrentCoordinates = () =>
105
+ new Promise(resolve => {
106
+ const geolocation =
107
+ typeof navigator !== 'undefined' ? navigator.geolocation : null;
108
+
109
+ if (!geolocation?.getCurrentPosition) {
110
+ resolve(null);
111
+ return;
112
+ }
113
+
114
+ geolocation.getCurrentPosition(
115
+ position => {
116
+ const latitude = Number(position?.coords?.latitude);
117
+ const longitude = Number(position?.coords?.longitude);
118
+
119
+ if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
120
+ resolve(null);
121
+ return;
122
+ }
123
+
124
+ resolve({latitude, longitude});
125
+ },
126
+ () => resolve(null),
127
+ {enableHighAccuracy: true, timeout: 8000, maximumAge: 60000},
128
+ );
129
+ });
130
+
131
+ export const buildMapMarkerPayload = form => {
132
+ if (!hasAddressText(form) && !hasCoordinates(form)) {
133
+ return null;
134
+ }
135
+
136
+ const addressLine = [form.street, form.number].filter(Boolean).join(', ');
137
+ const addressExtra = [
138
+ form.district,
139
+ [form.city, form.uf].filter(Boolean).join(' - '),
140
+ form.countryName || form.countryCode,
141
+ form.cep,
142
+ ]
143
+ .filter(Boolean)
144
+ .join(' • ');
145
+
146
+ return {
147
+ id: 'default-address-preview',
148
+ title: form.nickname || 'Endereco',
149
+ addressLine: addressLine || addressExtra || 'Endereco',
150
+ addressExtra,
151
+ latitude: form.latitude,
152
+ longitude: form.longitude,
153
+ geocodeQuery: [
154
+ addressLine,
155
+ form.district,
156
+ [form.city, form.uf].filter(Boolean).join(' - '),
157
+ form.countryName || form.countryCode,
158
+ form.cep,
159
+ ]
160
+ .filter(Boolean)
161
+ .join(', '),
162
+ };
163
+ };
@@ -0,0 +1,136 @@
1
+ import {StyleSheet} from 'react-native';
2
+
3
+ /**
4
+ * Styles for DefaultAddress — extracted to keep the component under 500 lines.
5
+ */
6
+ export const styles = StyleSheet.create({
7
+ scroll: {flex: 1, backgroundColor: '#fff'},
8
+ container: {padding: 16, paddingBottom: 32},
9
+ containerDesktop: {
10
+ flexDirection: 'row',
11
+ alignItems: 'flex-start',
12
+ gap: 24,
13
+ maxWidth: 1200,
14
+ alignSelf: 'center',
15
+ width: '100%',
16
+ },
17
+ formPane: {width: '100%'},
18
+ formPaneDesktop: {flex: 1, maxWidth: 560},
19
+ field: {marginBottom: 10, position: 'relative', zIndex: 1},
20
+ fieldRaised: {zIndex: 30, elevation: 8},
21
+ label: {fontSize: 13, fontWeight: '600', marginBottom: 4, color: '#334155'},
22
+ input: {
23
+ borderWidth: 1,
24
+ borderColor: '#CBD5E1',
25
+ borderRadius: 8,
26
+ paddingHorizontal: 12,
27
+ paddingVertical: 10,
28
+ backgroundColor: '#fff',
29
+ },
30
+ select: {
31
+ borderWidth: 1,
32
+ borderColor: '#CBD5E1',
33
+ borderRadius: 8,
34
+ paddingHorizontal: 12,
35
+ paddingVertical: 12,
36
+ backgroundColor: '#F8FAFC',
37
+ },
38
+ dropdown: {
39
+ maxHeight: 220,
40
+ borderWidth: 1,
41
+ borderColor: '#E2E8F0',
42
+ borderRadius: 8,
43
+ marginTop: 4,
44
+ backgroundColor: '#fff',
45
+ zIndex: 40,
46
+ elevation: 8,
47
+ },
48
+ option: {
49
+ paddingHorizontal: 12,
50
+ paddingVertical: 10,
51
+ borderBottomWidth: 1,
52
+ borderBottomColor: '#F1F5F9',
53
+ },
54
+ row: {flexDirection: 'row', alignItems: 'center'},
55
+ flex: {flex: 1},
56
+ half: {flex: 1},
57
+ gap8: {gap: 8},
58
+ loader: {marginLeft: 8},
59
+ error: {color: '#B91C1C', marginBottom: 8},
60
+ hint: {color: '#64748B', marginBottom: 8, fontSize: 12},
61
+ mapPane: {
62
+ width: '100%',
63
+ borderWidth: 1,
64
+ borderColor: '#E2E8F0',
65
+ borderRadius: 12,
66
+ backgroundColor: '#F8FAFC',
67
+ padding: 16,
68
+ gap: 12,
69
+ },
70
+ mapPaneDesktop: {flex: 1, minHeight: 560},
71
+ mapPaneTitle: {fontWeight: '700', fontSize: 15, color: '#0F172A'},
72
+ mapImage: {
73
+ width: '100%',
74
+ height: 220,
75
+ borderRadius: 10,
76
+ backgroundColor: '#E2E8F0',
77
+ },
78
+ mapImageDesktop: {height: 360},
79
+ liveMap: {
80
+ width: '100%',
81
+ height: 220,
82
+ borderRadius: 10,
83
+ overflow: 'hidden',
84
+ backgroundColor: '#E2E8F0',
85
+ },
86
+ liveMapDesktop: {height: 360},
87
+ facadeImage: {
88
+ width: '100%',
89
+ height: 180,
90
+ borderRadius: 10,
91
+ backgroundColor: '#E2E8F0',
92
+ },
93
+ mapPlaceholder: {
94
+ height: 220,
95
+ borderRadius: 10,
96
+ backgroundColor: '#E2E8F0',
97
+ alignItems: 'center',
98
+ justifyContent: 'center',
99
+ padding: 18,
100
+ },
101
+ facadePlaceholder: {
102
+ minHeight: 120,
103
+ borderRadius: 10,
104
+ backgroundColor: '#E2E8F0',
105
+ alignItems: 'center',
106
+ justifyContent: 'center',
107
+ padding: 18,
108
+ },
109
+ mapPlaceholderTitle: {
110
+ fontWeight: '700',
111
+ color: '#334155',
112
+ marginBottom: 6,
113
+ },
114
+ mapPlaceholderText: {
115
+ color: '#64748B',
116
+ textAlign: 'center',
117
+ fontSize: 13,
118
+ lineHeight: 18,
119
+ },
120
+ actions: {
121
+ flexDirection: 'row',
122
+ justifyContent: 'flex-end',
123
+ gap: 12,
124
+ marginTop: 16,
125
+ },
126
+ btnSecondary: {paddingHorizontal: 16, paddingVertical: 12},
127
+ btnPrimary: {
128
+ backgroundColor: '#2563EB',
129
+ borderRadius: 8,
130
+ paddingHorizontal: 18,
131
+ paddingVertical: 12,
132
+ },
133
+ btnPrimaryText: {color: '#fff', fontWeight: '600'},
134
+ btnSecondaryText: {color: '#475569', fontWeight: '600'},
135
+ btnDisabled: {opacity: 0.6},
136
+ });
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Helpers for DefaultAddress form (hydration, CEP merge, geolocation).
3
+ */
4
+ const emptyForm = {
5
+ nickname: '',
6
+ cep: '',
7
+ street: '',
8
+ number: '',
9
+ complement: '',
10
+ district: '',
11
+ city: '',
12
+ uf: '',
13
+ stateName: '',
14
+ countryCode: 'BR',
15
+ countryName: 'Brazil',
16
+ latitude: null,
17
+ longitude: null,
18
+ mapStaticUrl: null,
19
+ facadeUrl: null,
20
+ provider: null,
21
+ };
22
+
23
+ const hydrateFromRow = row => {
24
+ if (!row || typeof row !== 'object') {
25
+ return {...emptyForm};
26
+ }
27
+ const street = row?.street?.street || row?.street || '';
28
+ const district = row?.street?.district?.district || row?.district || '';
29
+ const city = row?.street?.district?.city?.city || row?.city || '';
30
+ const stateEntity = row?.street?.district?.city?.state || row?.state;
31
+ const countryEntity = stateEntity?.country || row?.country;
32
+ return {
33
+ ...emptyForm,
34
+ nickname: row?.nickname || '',
35
+ cep: String(row?.street?.cep?.cep || row?.cep || row?.postal_code || ''),
36
+ street: typeof street === 'string' ? street : '',
37
+ number: row?.number != null ? String(row.number) : '',
38
+ complement: row?.complement || '',
39
+ district,
40
+ city,
41
+ uf: stateEntity?.uf || row?.uf || '',
42
+ stateName: stateEntity?.state || '',
43
+ countryCode: countryEntity?.countrycode || countryEntity?.code || 'BR',
44
+ countryName: countryEntity?.countryname || countryEntity?.name || 'Brazil',
45
+ latitude: row?.latitude ?? null,
46
+ longitude: row?.longitude ?? null,
47
+ };
48
+ };
49
+
50
+ const onlyDigits = value => String(value || '').replace(/\D+/g, '');
51
+
52
+ const hasAddressText = form =>
53
+ [
54
+ form?.street,
55
+ form?.number,
56
+ form?.district,
57
+ form?.city,
58
+ form?.uf,
59
+ form?.cep,
60
+ ].some(value => String(value || '').trim().length > 0);
61
+
62
+ const hasCoordinates = form =>
63
+ Number.isFinite(Number(form?.latitude)) && Number.isFinite(Number(form?.longitude));
64
+
65
+ const mergePostalCodeData = (prev, data, {preserveFilledFields = false} = {}) => {
66
+ const keep = (key, nextValue) =>
67
+ preserveFilledFields && String(prev[key] || '').trim()
68
+ ? prev[key]
69
+ : nextValue || prev[key];
70
+
71
+ return {
72
+ ...prev,
73
+ cep: onlyDigits(data.cep || prev.cep),
74
+ street: keep('street', data.street),
75
+ district: keep('district', data.district),
76
+ city: keep('city', data.city),
77
+ uf: keep('uf', data.uf || data.state),
78
+ stateName: keep('stateName', data.state),
79
+ countryCode:
80
+ data.country === 'Brasil' || data.country === 'Brazil'
81
+ ? 'BR'
82
+ : data.country || prev.countryCode,
83
+ countryName:
84
+ data.country === 'Brasil' ? 'Brazil' : data.country || prev.countryName,
85
+ latitude: data.latitude ?? data.map?.latitude ?? prev.latitude,
86
+ longitude: data.longitude ?? data.map?.longitude ?? prev.longitude,
87
+ mapStaticUrl: data.map?.staticUrl || prev.mapStaticUrl || null,
88
+ facadeUrl: data.facade?.streetViewUrl || prev.facadeUrl || null,
89
+ provider: data.provider || prev.provider || null,
90
+ };
91
+ };
92
+
93
+ const getCurrentCoordinates = () =>
94
+ new Promise(resolve => {
95
+ const geolocation =
96
+ typeof navigator !== 'undefined' ? navigator.geolocation : null;
97
+
98
+ if (!geolocation?.getCurrentPosition) {
99
+ resolve(null);
100
+ return;
101
+ }
102
+
103
+ geolocation.getCurrentPosition(
104
+ position => {
105
+ const latitude = Number(position?.coords?.latitude);
106
+ const longitude = Number(position?.coords?.longitude);
107
+
108
+ if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
109
+ resolve(null);
110
+ return;
111
+ }
112
+
113
+ resolve({latitude, longitude});
114
+ },
115
+ () => resolve(null),
116
+ {enableHighAccuracy: true, timeout: 8000, maximumAge: 60000},
117
+ );
118
+ });
119
+
120
+
121
+ export {
122
+ emptyForm,
123
+ hydrateFromRow,
124
+ onlyDigits,
125
+ hasAddressText,
126
+ hasCoordinates,
127
+ mergePostalCodeData,
128
+ getCurrentCoordinates,
129
+ };