@archbase/components 4.0.41 → 4.0.42

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archbase/components",
3
- "version": "4.0.41",
3
+ "version": "4.0.42",
4
4
  "description": "UI Components for Archbase React v3 - Form editors, data visualization, and business components",
5
5
  "author": "Edson Martins <edsonmartins2005@gmail.com>",
6
6
  "license": "MIT",
@@ -118,9 +118,9 @@
118
118
  "vis-timeline": "^7.7.3",
119
119
  "xlsx": "^0.18.5",
120
120
  "yet-another-react-lightbox": "^3.21.0",
121
- "@archbase/core": "4.0.41",
122
- "@archbase/data": "4.0.41",
123
- "@archbase/layout": "4.0.41"
121
+ "@archbase/core": "4.0.42",
122
+ "@archbase/data": "4.0.42",
123
+ "@archbase/layout": "4.0.42"
124
124
  },
125
125
  "devDependencies": {
126
126
  "@types/d3": "^7.4.3",
@@ -9,12 +9,17 @@ import {
9
9
  } from '@mantine/core';
10
10
  import { useForceUpdate } from '@mantine/hooks';
11
11
  import type { CSSProperties, FocusEventHandler, ReactNode } from 'react';
12
- import React, { useCallback, useEffect, useRef, useState } from 'react';
12
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
13
13
  import type { ArchbaseDataSource, DataSourceEvent, IArchbaseDataSourceBase } from '@archbase/data';
14
14
  import { DataSourceEventNames } from '@archbase/data';
15
15
  import { useArchbaseDidUpdate } from '@archbase/data';
16
16
  import { useArchbaseV1V2Compatibility } from '@archbase/data';
17
- import { useValidationErrors } from '@archbase/core';
17
+ import type {
18
+ ArchbasePasswordPolicy,
19
+ ArchbasePasswordValidationResult,
20
+ } from '@archbase/core';
21
+ import { resolveArchbasePasswordPolicy, useValidationErrors, validateArchbasePassword } from '@archbase/core';
22
+ import { ArchbasePasswordStrengthMeter } from './ArchbasePasswordStrengthMeter';
18
23
 
19
24
  export interface ArchbasePasswordEditProps<T, ID> {
20
25
  /** Fonte de dados onde será atribuido o valor do edit (V1 ou V2) */
@@ -54,6 +59,24 @@ export interface ArchbasePasswordEditProps<T, ID> {
54
59
  /** Referência para o componente interno */
55
60
  innerRef?: React.RefObject<HTMLInputElement> | undefined;
56
61
  variant?: InputVariant;
62
+ /**
63
+ * Critérios de senha forte aplicados como pré-validação no próprio componente.
64
+ * `true` aplica a política padrão do Archbase; um objeto permite ajustar cada critério.
65
+ * Quando ausente nenhuma validação de força é feita (comportamento original).
66
+ */
67
+ passwordPolicy?: ArchbasePasswordPolicy | boolean;
68
+ /** Indicador se a lista de critérios deve ser exibida. Padrão: true quando há política */
69
+ showPasswordRequirements?: boolean;
70
+ /** Indicador se a barra de força deve ser exibida. Padrão: true quando há política */
71
+ showPasswordStrengthBar?: boolean;
72
+ /**
73
+ * Quando os critérios ficam visíveis:
74
+ * `onFocus` (padrão) enquanto o campo tem foco ou está inválido,
75
+ * `always` sempre, `whenInvalid` somente quando algum critério falha.
76
+ */
77
+ passwordRequirementsVisibility?: 'always' | 'onFocus' | 'whenInvalid';
78
+ /** Evento disparado a cada mudança no resultado da pré-validação */
79
+ onPasswordValidation?: (result: ArchbasePasswordValidationResult) => void;
57
80
  }
58
81
 
59
82
  export function ArchbasePasswordEdit<T, ID>({
@@ -77,6 +100,11 @@ export function ArchbasePasswordEdit<T, ID>({
77
100
  onFocusEnter = () => {},
78
101
  onChangeValue = () => {},
79
102
  variant,
103
+ passwordPolicy,
104
+ showPasswordRequirements = true,
105
+ showPasswordStrengthBar = true,
106
+ passwordRequirementsVisibility = 'onFocus',
107
+ onPasswordValidation,
80
108
  }: ArchbasePasswordEditProps<T, ID>) {
81
109
  // 🔄 MIGRAÇÃO V1/V2: Hook de compatibilidade
82
110
  const v1v2Compatibility = useArchbaseV1V2Compatibility<string>(
@@ -105,6 +133,22 @@ export function ArchbasePasswordEdit<T, ID>({
105
133
  const [internalError, setInternalError] = useState<string | undefined>(error);
106
134
  const forceUpdate = useForceUpdate();
107
135
 
136
+ // Pré-validação de senha forte: só entra em ação quando uma política é informada.
137
+ const resolvedPolicy = useMemo(() => resolveArchbasePasswordPolicy(passwordPolicy), [passwordPolicy]);
138
+ const [isFocused, setIsFocused] = useState<boolean>(false);
139
+ const [policyError, setPolicyError] = useState<string | undefined>(undefined);
140
+
141
+ const policyResult = useMemo(
142
+ () => (resolvedPolicy ? validateArchbasePassword(currentValue, resolvedPolicy) : undefined),
143
+ [currentValue, resolvedPolicy]
144
+ );
145
+
146
+ useEffect(() => {
147
+ if (policyResult && onPasswordValidation) {
148
+ onPasswordValidation(policyResult);
149
+ }
150
+ }, [policyResult]);
151
+
108
152
  // ❌ REMOVIDO: Não limpar erro automaticamente quando valor muda
109
153
  // O erro deve ser limpo apenas quando o usuário EDITA o campo (no handleChange)
110
154
  // useEffect(() => {
@@ -208,6 +252,11 @@ useEffect(() => {
208
252
  validationContext?.clearError(fieldKey);
209
253
  }
210
254
 
255
+ // O erro de política reaparece no blur; enquanto digita o feedback fica na lista de critérios.
256
+ if (policyError) {
257
+ setPolicyError(undefined);
258
+ }
259
+
211
260
  setCurrentValue((_prev) => changedValue);
212
261
 
213
262
  if (dataSource && !dataSource.isBrowsing() && dataField && dataSource.getFieldValue(dataField) !== changedValue) {
@@ -221,12 +270,18 @@ useEffect(() => {
221
270
  };
222
271
 
223
272
  const handleOnFocusExit = (event) => {
273
+ setIsFocused(false);
274
+ // Só acusa erro depois que o usuário terminou de digitar e o campo tem conteúdo.
275
+ if (policyResult && currentValue) {
276
+ setPolicyError(policyResult.error);
277
+ }
224
278
  if (onFocusExit) {
225
279
  onFocusExit(event);
226
280
  }
227
281
  };
228
282
 
229
283
  const handleOnFocusEnter = (event) => {
284
+ setIsFocused(true);
230
285
  if (onFocusEnter) {
231
286
  onFocusEnter(event);
232
287
  }
@@ -237,10 +292,24 @@ useEffect(() => {
237
292
  return readOnly || v1v2Compatibility.isReadOnly;
238
293
  };
239
294
 
240
- // Erro a ser exibido: local ou do contexto
241
- const displayError = internalError || contextError;
295
+ // Erro a ser exibido: local, do contexto ou da política de senha
296
+ const displayError = internalError || contextError || policyError;
242
297
 
243
- return (
298
+ const shouldShowPolicyFeedback = (): boolean => {
299
+ if (!policyResult || isReadOnly() || disabled) {
300
+ return false;
301
+ }
302
+ switch (passwordRequirementsVisibility) {
303
+ case 'always':
304
+ return true;
305
+ case 'whenInvalid':
306
+ return currentValue.length > 0 && !policyResult.valid;
307
+ default:
308
+ return isFocused || (currentValue.length > 0 && !policyResult.valid);
309
+ }
310
+ };
311
+
312
+ const passwordInput = (
244
313
  <PasswordInput
245
314
  disabled={disabled}
246
315
  readOnly={isReadOnly()}
@@ -263,4 +332,22 @@ useEffect(() => {
263
332
  error={displayError}
264
333
  />
265
334
  );
335
+
336
+ if (!resolvedPolicy) {
337
+ return passwordInput;
338
+ }
339
+
340
+ return (
341
+ <div style={{ width }}>
342
+ {passwordInput}
343
+ {shouldShowPolicyFeedback() && (
344
+ <ArchbasePasswordStrengthMeter
345
+ value={currentValue}
346
+ policy={resolvedPolicy}
347
+ showStrengthBar={showPasswordStrengthBar}
348
+ showRequirements={showPasswordRequirements}
349
+ />
350
+ )}
351
+ </div>
352
+ );
266
353
  }
@@ -0,0 +1,112 @@
1
+ import { Box, Group, MantineSize, Progress, Stack, Text } from '@mantine/core';
2
+ import { IconCheck, IconX } from '@tabler/icons-react';
3
+ import type { CSSProperties } from 'react';
4
+ import React, { useEffect, useMemo, useRef } from 'react';
5
+ import type {
6
+ ArchbasePasswordPolicy,
7
+ ArchbasePasswordStrength,
8
+ ArchbasePasswordValidationResult,
9
+ } from '@archbase/core';
10
+ import {
11
+ getArchbasePasswordStrengthLabel,
12
+ resolveArchbasePasswordPolicy,
13
+ validateArchbasePassword,
14
+ } from '@archbase/core';
15
+
16
+ export interface ArchbasePasswordStrengthMeterProps {
17
+ /** Senha a ser avaliada */
18
+ value?: string;
19
+ /** Critérios de senha forte. `true` aplica a política padrão do Archbase */
20
+ policy?: ArchbasePasswordPolicy | boolean;
21
+ /** Indicador se a barra de força deve ser exibida */
22
+ showStrengthBar?: boolean;
23
+ /** Indicador se a lista de critérios deve ser exibida */
24
+ showRequirements?: boolean;
25
+ /** Exibe somente os critérios ainda não atendidos */
26
+ onlyUnmetRequirements?: boolean;
27
+ /** Tamanho do texto dos critérios */
28
+ size?: MantineSize;
29
+ /** Evento disparado sempre que o resultado da validação muda */
30
+ onValidationChange?: (result: ArchbasePasswordValidationResult) => void;
31
+ /** Estilo do container */
32
+ style?: CSSProperties;
33
+ }
34
+
35
+ const STRENGTH_COLORS: Record<ArchbasePasswordStrength, string> = {
36
+ empty: 'gray',
37
+ weak: 'red',
38
+ fair: 'orange',
39
+ good: 'yellow',
40
+ strong: 'teal',
41
+ };
42
+
43
+ export function ArchbasePasswordStrengthMeter({
44
+ value = '',
45
+ policy = true,
46
+ showStrengthBar = true,
47
+ showRequirements = true,
48
+ onlyUnmetRequirements = false,
49
+ size = 'xs',
50
+ onValidationChange,
51
+ style,
52
+ }: ArchbasePasswordStrengthMeterProps) {
53
+ const resolvedPolicy = useMemo(() => resolveArchbasePasswordPolicy(policy), [policy]);
54
+
55
+ const result = useMemo(
56
+ () => (resolvedPolicy ? validateArchbasePassword(value, resolvedPolicy) : undefined),
57
+ [value, resolvedPolicy]
58
+ );
59
+
60
+ // Notifica apenas quando o resultado realmente muda, evitando loops de render no consumidor.
61
+ const lastNotifiedRef = useRef<string | undefined>(undefined);
62
+ useEffect(() => {
63
+ if (!result || !onValidationChange) {
64
+ return;
65
+ }
66
+ const signature = `${result.valid}|${result.score}|${result.unmetRequirements
67
+ .map((requirement) => requirement.key)
68
+ .join(',')}`;
69
+ if (lastNotifiedRef.current !== signature) {
70
+ lastNotifiedRef.current = signature;
71
+ onValidationChange(result);
72
+ }
73
+ }, [result, onValidationChange]);
74
+
75
+ if (!result) {
76
+ return null;
77
+ }
78
+
79
+ const color = STRENGTH_COLORS[result.strength];
80
+ const requirements = onlyUnmetRequirements ? result.unmetRequirements : result.requirements;
81
+
82
+ return (
83
+ <Stack gap={4} mt={6} style={style}>
84
+ {showStrengthBar && (
85
+ <Box>
86
+ <Progress value={result.score} color={color} size="sm" radius="xl" />
87
+ {result.strength !== 'empty' && (
88
+ <Text size={size} c={color} mt={2}>
89
+ {getArchbasePasswordStrengthLabel(result.strength)}
90
+ </Text>
91
+ )}
92
+ </Box>
93
+ )}
94
+ {showRequirements && requirements.length > 0 && (
95
+ <Stack gap={2}>
96
+ {requirements.map((requirement) => (
97
+ <Group key={requirement.key} gap={6} wrap="nowrap" align="center">
98
+ {requirement.satisfied ? (
99
+ <IconCheck size={14} color="var(--mantine-color-teal-6)" />
100
+ ) : (
101
+ <IconX size={14} color="var(--mantine-color-red-6)" />
102
+ )}
103
+ <Text size={size} c={requirement.satisfied ? 'teal' : 'dimmed'}>
104
+ {requirement.label}
105
+ </Text>
106
+ </Group>
107
+ ))}
108
+ </Stack>
109
+ )}
110
+ </Stack>
111
+ );
112
+ }
@@ -63,6 +63,8 @@ export type { ArchbaseNumberEditProps } from './ArchbaseNumberEdit';
63
63
 
64
64
  export { ArchbasePasswordEdit } from './ArchbasePasswordEdit';
65
65
  export type { ArchbasePasswordEditProps } from './ArchbasePasswordEdit';
66
+ export { ArchbasePasswordStrengthMeter } from './ArchbasePasswordStrengthMeter';
67
+ export type { ArchbasePasswordStrengthMeterProps } from './ArchbasePasswordStrengthMeter';
66
68
 
67
69
  export { ArchbaseRadioGroup } from './ArchbaseRadioGroup';
68
70
  export type { ArchbaseRadioGroupProps } from './ArchbaseRadioGroup';
Binary file