@controleonline/ui-default 1.0.266 → 1.0.268

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/AGENTS.md CHANGED
@@ -21,8 +21,9 @@
21
21
  - `requestParams` serve para contexto fixo do fluxo, nao para duplicar estado de filtro que ja vive no store.
22
22
  - `pageSize` so deve ser usado quando o contrato externo exigir e com justificativa clara no modulo.
23
23
  - `summary` e `totalItems` vem do backend/store e pertencem ao rodape interno do `DefaultTable`.
24
- - `add: true` abre o `DefaultForm` quando a tela nao prover `onAdd`.
25
- - O modo controlado continua existindo apenas para excecoes reais de integracao; o padrao do sistema e o modo store-driven.
24
+ - `add: true` abre o `DefaultForm` quando a tela nao prover `onAdd`.
25
+ - `rowActionsComponent` e o slot canonico para acoes por linha quando a tela precisa renderizar mais do que o botao padrao de edicao. Ele pode ser componente, funcao ou elemento React, recebe `row`, `storeName`, `openEdit`, `openRow` e `helpers`, e a largura da coluna deve ser ajustada com `rowActionsWidth` quando houver mais de uma acao.
26
+ - O modo controlado continua existindo apenas para excecoes reais de integracao; o padrao do sistema e o modo store-driven.
26
27
  - Loadings e erros devem seguir o fluxo central do sistema, sem banners ou estados paralelos na tela.
27
28
  - O `get` do store default pode preservar o `item` atual durante refresh quando receber `__storeMeta.preserveItem = true`; use esse modo quando a tela precisa atualizar o registro sem desmontar o detalhe em exibição.
28
29
  - Explicacoes permanentes de tela devem sair do corpo principal e ir para o `DefaultTooltip`, acionado por `?`, quando houver contexto necessario para o usuario.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@controleonline/ui-default",
3
- "version": "1.0.266",
3
+ "version": "1.0.268",
4
4
  "author": "ControleOnline",
5
5
  "description": "ControleOnline Default Quasar UI Component",
6
6
  "license": "MIT",
@@ -1,5 +1,6 @@
1
1
  import React, { useCallback, useMemo, useState } from 'react';
2
- import {
2
+ import {
3
+ Image,
3
4
  Modal,
4
5
  ScrollView,
5
6
  Text,
@@ -10,12 +11,13 @@ import {
10
11
  View,
11
12
  } from 'react-native';
12
13
  import Icon from 'react-native-vector-icons/Feather';
13
- import {
14
- formatStoreColumnLabel,
15
- resolveStoreColumn,
16
- resolveStoreConfigByName,
17
- } from '@controleonline/ui-common/src/react/utils/storeColumns';
18
- import createStyles from './CompactFilterSelector.styles';
14
+ import {
15
+ formatStoreColumnLabel,
16
+ resolveStoreColumn,
17
+ resolveStoreConfigByName,
18
+ } from '@controleonline/ui-common/src/react/utils/storeColumns';
19
+ import { isValidFeatherIcon } from '../inputs/defaultInputUtils';
20
+ import createStyles from './CompactFilterSelector.styles';
19
21
 
20
22
  const buildTheme = ({ accentColor, themeColors = {} }) => ({
21
23
  accentColor: accentColor,
@@ -117,8 +119,23 @@ const CompactFilterSelector = ({
117
119
  field,
118
120
  store,
119
121
  });
120
- const resolvedLabelCaption = labelCaption || storeFieldLabel;
121
- const resolvedTitle = title || storeFieldLabel;
122
+ const resolvedLabelCaption = labelCaption || storeFieldLabel;
123
+ const resolvedTitle = title || storeFieldLabel;
124
+ const selectedOption = useMemo(
125
+ () => options.find(option => option?.key === selectedKey),
126
+ [options, selectedKey],
127
+ );
128
+ const selectedOptionColor = normalizeText(selectedOption?.color);
129
+ const selectedOptionIcon = normalizeText(selectedOption?.icon);
130
+ const selectedOptionIconName = isValidFeatherIcon(selectedOptionIcon)
131
+ ? selectedOptionIcon
132
+ : '';
133
+ const triggerIconName = active && selectedOptionIconName
134
+ ? selectedOptionIconName
135
+ : icon;
136
+ const triggerIconColor = active
137
+ ? selectedOptionColor || resolvedTheme.activeIconColor
138
+ : resolvedTheme.iconColor;
122
139
 
123
140
  const closeModal = useCallback(() => {
124
141
  setVisible(false);
@@ -201,22 +218,31 @@ const CompactFilterSelector = ({
201
218
  disabled={disabled}
202
219
  onPress={openModal}
203
220
  >
204
- <View style={[styles.iconWrap, active ? styles.iconWrapActive : null]}>
205
- <Icon
206
- name={icon}
207
- size={dense ? 14 : 15}
208
- color={active ? resolvedTheme.activeIconColor : resolvedTheme.iconColor}
209
- />
210
- </View>
221
+ <View style={[styles.iconWrap, active ? styles.iconWrapActive : null]}>
222
+ {active && selectedOption?.image ? (
223
+ <Image
224
+ source={selectedOption.image}
225
+ style={styles.triggerIconImage}
226
+ resizeMode="contain"
227
+ />
228
+ ) : (
229
+ <Icon
230
+ name={triggerIconName}
231
+ size={dense ? 14 : 15}
232
+ color={triggerIconColor}
233
+ />
234
+ )}
235
+ </View>
211
236
 
212
237
  <View style={styles.textWrap}>
213
238
  {!!resolvedLabelCaption && (
214
239
  <Text
215
240
  numberOfLines={1}
216
241
  style={[
217
- styles.triggerCaption,
218
- active ? styles.triggerCaptionActive : null,
219
- ]}
242
+ styles.triggerCaption,
243
+ active ? styles.triggerCaptionActive : null,
244
+ active && selectedOptionColor ? { color: selectedOptionColor } : null,
245
+ ]}
220
246
  >
221
247
  {resolvedLabelCaption}
222
248
  </Text>
@@ -224,7 +250,11 @@ const CompactFilterSelector = ({
224
250
 
225
251
  <Text
226
252
  numberOfLines={1}
227
- style={[styles.triggerText, active ? styles.triggerTextActive : null]}
253
+ style={[
254
+ styles.triggerText,
255
+ active ? styles.triggerTextActive : null,
256
+ active && selectedOptionColor ? { color: selectedOptionColor } : null,
257
+ ]}
228
258
  >
229
259
  {label}
230
260
  </Text>
@@ -270,33 +300,74 @@ const CompactFilterSelector = ({
270
300
  ) : null}
271
301
 
272
302
  {filteredOptions.map(option => {
273
- const isSelected = option.key === selectedKey;
274
-
275
- return (
303
+ const isSelected = option.key === selectedKey;
304
+ const optionColor = normalizeText(option?.color);
305
+ const optionIcon = normalizeText(option?.icon);
306
+ const optionIconName = isValidFeatherIcon(optionIcon)
307
+ ? optionIcon
308
+ : '';
309
+ const shouldRenderOptionFallbackText = Boolean(
310
+ !option?.image &&
311
+ !optionIconName &&
312
+ optionColor &&
313
+ normalizeText(option?.label),
314
+ );
315
+ const hasOptionVisual = Boolean(option?.image || optionIconName || shouldRenderOptionFallbackText);
316
+
317
+ return (
276
318
  <TouchableOpacity
277
319
  key={option.key}
278
320
  style={[
279
321
  styles.modalOption,
280
322
  isSelected ? styles.modalOptionActive : null,
281
323
  ]}
282
- activeOpacity={0.9}
283
- onPress={() => handleSelect(option.key)}
284
- >
285
- <Text
286
- style={[
287
- styles.modalOptionText,
288
- isSelected ? styles.modalOptionTextActive : null,
289
- ]}
290
- >
291
- {option.label}
292
- </Text>
324
+ activeOpacity={0.9}
325
+ onPress={() => handleSelect(option.key)}
326
+ >
327
+ {option?.image ? (
328
+ <Image
329
+ source={option.image}
330
+ style={styles.modalOptionImage}
331
+ resizeMode="contain"
332
+ />
333
+ ) : optionIconName ? (
334
+ <Icon
335
+ style={styles.modalOptionIcon}
336
+ name={optionIconName}
337
+ size={16}
338
+ color={optionColor || resolvedTheme.iconColor}
339
+ />
340
+ ) : shouldRenderOptionFallbackText ? (
341
+ <Text
342
+ style={[
343
+ styles.modalOptionFallbackText,
344
+ { color: optionColor },
345
+ ]}
346
+ numberOfLines={1}
347
+ >
348
+ {option.label}
349
+ </Text>
350
+ ) : null}
351
+
352
+ {shouldRenderOptionFallbackText ? null : (
353
+ <Text
354
+ style={[
355
+ styles.modalOptionText,
356
+ isSelected ? styles.modalOptionTextActive : null,
357
+ optionColor ? { color: optionColor } : null,
358
+ !hasOptionVisual ? styles.modalOptionTextNoVisual : null,
359
+ ]}
360
+ >
361
+ {option.label}
362
+ </Text>
363
+ )}
293
364
 
294
365
  {isSelected ? (
295
- <Icon
296
- name="check"
297
- size={16}
298
- color={resolvedTheme.activeIconColor}
299
- />
366
+ <Icon
367
+ name="check"
368
+ size={16}
369
+ color={optionColor || resolvedTheme.activeIconColor}
370
+ />
300
371
  ) : null}
301
372
  </TouchableOpacity>
302
373
  );
@@ -29,9 +29,14 @@ const createStyles = (theme, dense = false, windowWidth = 0) => {
29
29
  justifyContent: 'center',
30
30
  backgroundColor: theme.iconBackgroundColor,
31
31
  },
32
- iconWrapActive: {
33
- backgroundColor: theme.activeIconBackgroundColor,
34
- },
32
+ iconWrapActive: {
33
+ backgroundColor: theme.activeIconBackgroundColor,
34
+ },
35
+ triggerIconImage: {
36
+ width: dense ? 16 : 18,
37
+ height: dense ? 16 : 18,
38
+ flexShrink: 0,
39
+ },
35
40
  textWrap: {
36
41
  flex: 1,
37
42
  minWidth: 0,
@@ -105,7 +110,7 @@ const createStyles = (theme, dense = false, windowWidth = 0) => {
105
110
  fontWeight: '700',
106
111
  },
107
112
  modalOption: {
108
- minHeight: 50,
113
+ minHeight: 50,
109
114
  borderWidth: 1,
110
115
  borderColor: theme.optionBorderColor,
111
116
  borderRadius: 16,
@@ -115,18 +120,35 @@ const createStyles = (theme, dense = false, windowWidth = 0) => {
115
120
  flexDirection: 'row',
116
121
  alignItems: 'center',
117
122
  justifyContent: 'space-between',
118
- gap: 8,
119
- },
120
- modalOptionActive: {
121
- borderColor: theme.accentColor,
122
- backgroundColor: theme.activeBackgroundColor,
123
+ gap: 8,
124
+ },
125
+ modalOptionIcon: {
126
+ flexShrink: 0,
127
+ },
128
+ modalOptionImage: {
129
+ width: 18,
130
+ height: 18,
131
+ flexShrink: 0,
132
+ },
133
+ modalOptionFallbackText: {
134
+ flex: 1,
135
+ minWidth: 0,
136
+ fontSize: 14,
137
+ fontWeight: '800',
138
+ },
139
+ modalOptionActive: {
140
+ borderColor: theme.accentColor,
141
+ backgroundColor: theme.activeBackgroundColor,
123
142
  },
124
143
  modalOptionText: {
125
144
  flex: 1,
126
145
  fontSize: 14,
127
- fontWeight: '700',
128
- color: theme.textColor,
129
- },
146
+ fontWeight: '700',
147
+ color: theme.textColor,
148
+ },
149
+ modalOptionTextNoVisual: {
150
+ marginLeft: 0,
151
+ },
130
152
  modalOptionTextActive: {
131
153
  color: theme.optionSelectedTextColor,
132
154
  },
@@ -1,19 +1,22 @@
1
1
  import React, { useEffect, useMemo, useState } from 'react';
2
- import { Text, TextInput, TouchableOpacity, View } from 'react-native';
2
+ import { Image, Text, TextInput, TouchableOpacity, View } from 'react-native';
3
3
  import Icon from 'react-native-vector-icons/Feather';
4
- import {
4
+ import {
5
+ buildReadPresentationStyles,
5
6
  getColumnKey,
6
7
  isDateLikeColumn,
7
8
  isEditableColumn,
8
- normalizeText,
9
- resolveCellText,
10
- resolveEditValue,
9
+ isValidFeatherIcon,
10
+ normalizeText,
11
+ resolveCellPresentation,
12
+ resolveCellText,
13
+ resolveEditValue,
11
14
  } from './defaultInputUtils';
12
15
  import DefaultDateInput from './DefaultDateInput';
13
16
  import DefaultSelect from './DefaultSelect';
14
- import styles from './DefaultInput.styles';
15
-
16
- const DefaultInput = ({
17
+ import styles from './DefaultInput.styles';
18
+
19
+ const DefaultInput = ({
17
20
  accentColor = '#2563EB',
18
21
  autoFocus = true,
19
22
  autoSave = true,
@@ -48,8 +51,19 @@ const DefaultInput = ({
48
51
  [column, row, value],
49
52
  );
50
53
  const [draftValue, setDraftValue] = useState(editValue);
51
- const resolvedLabel =
52
- displayValue ?? resolveCellText({ column, columns, row, storeName, value });
54
+ const resolvedLabel =
55
+ displayValue ?? resolveCellText({ column, columns, row, storeName, value });
56
+ const readPresentation = useMemo(
57
+ () => resolveCellPresentation({ column, columns, row, storeName, value }),
58
+ [column, columns, row, storeName, value],
59
+ );
60
+ const readPresentationStyles = useMemo(
61
+ () => buildReadPresentationStyles({
62
+ ...readPresentation,
63
+ label: resolvedLabel,
64
+ }),
65
+ [readPresentation, resolvedLabel],
66
+ );
53
67
 
54
68
  useEffect(() => {
55
69
  setDraftValue(editValue);
@@ -128,31 +142,81 @@ const DefaultInput = ({
128
142
  if (!autoSave) onChangeValue?.(nextValue);
129
143
  };
130
144
 
131
- if (!editing && !isForm) {
132
- return (
133
- <View style={[styles.wrap, containerStyle]}>
134
- {showLabel ? <Text style={styles.fieldLabel}>{label || column?.label || fieldName}</Text> : null}
135
- <TouchableOpacity
145
+ if (!editing && !isForm) {
146
+ const readIconName = isValidFeatherIcon(readPresentation.icon)
147
+ ? readPresentation.icon
148
+ : '';
149
+ const shouldRenderIconFallbackText = Boolean(
150
+ readPresentationStyles.hasDecoration &&
151
+ !readIconName &&
152
+ readPresentationStyles.color &&
153
+ normalizeText(resolvedLabel) &&
154
+ resolvedLabel !== '-',
155
+ );
156
+
157
+ return (
158
+ <View style={[styles.wrap, containerStyle]}>
159
+ {showLabel ? <Text style={styles.fieldLabel}>{label || column?.label || fieldName}</Text> : null}
160
+ <TouchableOpacity
136
161
  style={styles.readButton}
137
162
  activeOpacity={canEdit ? 0.78 : 1}
138
- disabled={!canEdit}
139
- onPress={() => onStartEditing?.()}
140
- >
141
- <Text
142
- style={[
143
- styles.readText,
144
- resolvedLabel === '-' ? styles.mutedText : null,
145
- readTextStyle,
146
- ]}
147
- numberOfLines={numberOfLines}
148
- >
149
- {column?.prefix || ''}
150
- {resolvedLabel || '-'}
151
- {column?.sufix || column?.suffix || ''}
152
- </Text>
153
- {saving ? (
154
- <Text style={[styles.savingText, { color: accentColor }]}>Salvando</Text>
155
- ) : canEdit ? (
163
+ disabled={!canEdit}
164
+ onPress={() => onStartEditing?.()}
165
+ >
166
+ {shouldRenderIconFallbackText ? (
167
+ <Text
168
+ style={[
169
+ styles.readText,
170
+ styles.readTextBadge,
171
+ readPresentationStyles.color ? { color: readPresentationStyles.color } : null,
172
+ readTextStyle,
173
+ ]}
174
+ numberOfLines={numberOfLines}
175
+ >
176
+ {resolvedLabel}
177
+ </Text>
178
+ ) : (
179
+ <View
180
+ style={[
181
+ styles.readValueWrap,
182
+ readPresentationStyles.hasDecoration ? styles.readBadge : null,
183
+ readPresentationStyles.badgeStyle,
184
+ ]}
185
+ >
186
+ {readPresentation.image ? (
187
+ <Image
188
+ source={readPresentation.image}
189
+ style={styles.readImage}
190
+ resizeMode="contain"
191
+ />
192
+ ) : null}
193
+ {readPresentationStyles.hasDecoration && readIconName && readPresentationStyles.color ? (
194
+ <Icon
195
+ style={styles.readBadgeIcon}
196
+ name={readIconName}
197
+ size={12}
198
+ color={readPresentationStyles.color}
199
+ />
200
+ ) : null}
201
+ <Text
202
+ style={[
203
+ styles.readText,
204
+ readPresentationStyles.hasDecoration ? styles.readBadgeText : null,
205
+ resolvedLabel === '-' ? styles.mutedText : null,
206
+ readPresentationStyles.color ? { color: readPresentationStyles.color } : null,
207
+ readTextStyle,
208
+ ]}
209
+ numberOfLines={numberOfLines}
210
+ >
211
+ {column?.prefix || ''}
212
+ {resolvedLabel || '-'}
213
+ {column?.sufix || column?.suffix || ''}
214
+ </Text>
215
+ </View>
216
+ )}
217
+ {saving ? (
218
+ <Text style={[styles.savingText, { color: accentColor }]}>Salvando</Text>
219
+ ) : canEdit ? (
156
220
  <Icon style={styles.editIcon} name="edit-2" size={13} color="#64748B" />
157
221
  ) : null}
158
222
  </TouchableOpacity>
@@ -13,6 +13,44 @@ const styles = StyleSheet.create({
13
13
  alignItems: 'center',
14
14
  gap: 6,
15
15
  },
16
+ readValueWrap: {
17
+ flex: 1,
18
+ minWidth: 0,
19
+ maxWidth: '100%',
20
+ flexDirection: 'row',
21
+ alignItems: 'center',
22
+ gap: 5,
23
+ },
24
+ readBadge: {
25
+ flex: 0,
26
+ alignSelf: 'flex-start',
27
+ minWidth: 22,
28
+ minHeight: 22,
29
+ maxWidth: '100%',
30
+ borderWidth: 1,
31
+ borderRadius: 6,
32
+ paddingHorizontal: 7,
33
+ paddingVertical: 2,
34
+ },
35
+ readBadgeText: {
36
+ flex: 0,
37
+ flexShrink: 0,
38
+ maxWidth: '100%',
39
+ fontSize: 11,
40
+ fontWeight: '800',
41
+ },
42
+ readTextBadge: {
43
+ flex: 1,
44
+ maxWidth: '100%',
45
+ },
46
+ readBadgeIcon: {
47
+ flexShrink: 0,
48
+ },
49
+ readImage: {
50
+ width: 16,
51
+ height: 16,
52
+ flexShrink: 0,
53
+ },
16
54
  readButtonForm: {
17
55
  minHeight: 34,
18
56
  borderWidth: 1,
@@ -25,8 +63,8 @@ const styles = StyleSheet.create({
25
63
  flex: 1,
26
64
  minWidth: 0,
27
65
  color: '#0F172A',
28
- fontSize: 12,
29
- fontWeight: '700',
66
+ fontSize: 13,
67
+ fontWeight: '600',
30
68
  },
31
69
  mutedText: {
32
70
  color: '#94A3B8',
@@ -62,8 +100,8 @@ const styles = StyleSheet.create({
62
100
  borderRadius: 8,
63
101
  paddingHorizontal: 8,
64
102
  color: '#0F172A',
65
- fontSize: 12,
66
- fontWeight: '700',
103
+ fontSize: 13,
104
+ fontWeight: '600',
67
105
  backgroundColor: '#FFFFFF',
68
106
  },
69
107
  formInput: {