rl-core-front 0.3.0 → 0.5.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/package.json +1 -1
- package/src/_services/api/app-error-code.enum.ts +23 -0
- package/src/_services/api/schema.d.ts +86 -132
- package/src/_services/auth/index.ts +2 -0
- package/src/_utils/filter.ts +15 -34
- package/src/_utils/password.ts +40 -0
- package/src/_utils/permission.ts +14 -0
- package/src/_utils/storage.ts +64 -0
- package/src/_utils/user.ts +19 -0
- package/src/components/app-shell.tsx +85 -11
- package/src/components/brand-header.tsx +23 -8
- package/src/components/brand-panel.tsx +41 -0
- package/src/components/index.ts +1 -0
- package/src/components/password-field.tsx +49 -22
- package/src/components/ui/alert.tsx +34 -5
- package/src/components/ui/badge.tsx +2 -2
- package/src/components/ui/button.tsx +5 -0
- package/src/components/ui/confirm-dialog.tsx +3 -1
- package/src/components/ui/data-table.tsx +13 -4
- package/src/components/ui/dialog.tsx +120 -21
- package/src/components/ui/filter-sheet.tsx +47 -25
- package/src/components/ui/index.ts +3 -1
- package/src/components/ui/password-requirements.tsx +105 -0
- package/src/components/ui/segmented-control.tsx +75 -0
- package/src/components/ui/simple-select.tsx +74 -0
- package/src/components/ui/switch.tsx +3 -1
- package/src/components/ui/tabs.tsx +4 -8
- package/src/components/ui/track.styles.ts +31 -0
- package/src/contexts/brand-context.tsx +15 -0
- package/src/contexts/color-mode-context.tsx +4 -2
- package/src/contexts/i18n-context.tsx +72 -6
- package/src/contexts/socket-context.tsx +27 -1
- package/src/features/audit/audit-screen.tsx +13 -140
- package/src/features/audit/components/audit-diff.tsx +156 -0
- package/src/features/audit/components/index.ts +3 -0
- package/src/features/audit/components/record-audit-button.tsx +176 -0
- package/src/features/audit/enums/audit-data-action.enum.ts +20 -0
- package/src/features/audit/hooks/use-audit-trail.ts +9 -2
- package/src/features/audit/services/audit.service.ts +43 -1
- package/src/features/login/components/backup-codes-dialog.tsx +4 -4
- package/src/features/login/components/login-form.tsx +18 -3
- package/src/features/login/components/two-factor-setup.tsx +32 -2
- package/src/features/login/hooks/use-login-flow.ts +60 -7
- package/src/features/login/login-screen.tsx +99 -40
- package/src/features/logs/enums/error-type.enum.ts +21 -0
- package/src/features/logs/enums/request-outcome.enum.ts +14 -0
- package/src/features/logs/hooks/use-request-logs.ts +26 -3
- package/src/features/logs/logs-screen.tsx +2 -14
- package/src/features/logs/panels/index.ts +0 -1
- package/src/features/logs/panels/request-logs-panel.tsx +177 -25
- package/src/features/logs/services/logs.service.ts +13 -51
- package/src/features/notifications/components/notification-item.tsx +10 -8
- package/src/features/notifications/enums/notification-type.enum.ts +8 -0
- package/src/features/profile/force-password-change.tsx +18 -3
- package/src/features/profile/profile-screen.tsx +10 -2
- package/src/features/rbac/panels/groups-panel.tsx +78 -30
- package/src/features/rbac/panels/roles-panel.tsx +2 -2
- package/src/features/rbac/services/rbac.service.ts +23 -27
- package/src/features/recovery/reset-password-screen.tsx +16 -2
- package/src/features/users/components/user-form-dialog.tsx +65 -9
- package/src/features/users/hooks/use-users.ts +24 -3
- package/src/features/users/services/users.service.ts +16 -0
- package/src/features/users/users-screen.tsx +17 -12
- package/src/hooks/use-column-visibility.ts +13 -12
- package/src/hooks/use-countdown.ts +70 -0
- package/src/hooks/use-filters.ts +25 -2
- package/src/hooks/use-list-query.ts +14 -1
- package/src/hooks/use-request.ts +21 -2
- package/src/i18n/messages/en.ts +37 -6
- package/src/i18n/messages/pt.ts +41 -6
- package/src/index.ts +15 -1
- package/src/styles/core.css +99 -2
- package/src/styles/fonts/poppins-latin-400.woff2 +0 -0
- package/src/styles/fonts/poppins-latin-500.woff2 +0 -0
- package/src/styles/fonts/poppins-latin-600.woff2 +0 -0
- package/src/styles/fonts/poppins-latin-700.woff2 +0 -0
- package/tailwind-preset.ts +17 -0
- package/src/components/ui/filter-chips.tsx +0 -88
- package/src/features/logs/hooks/use-error-logs.ts +0 -65
- package/src/features/logs/panels/error-logs-panel.tsx +0 -183
|
@@ -5,12 +5,22 @@ import type { Dispatch, SetStateAction } from "react";
|
|
|
5
5
|
import { useEffect, useMemo, useState } from "react";
|
|
6
6
|
|
|
7
7
|
import type { ListQuery } from "#core/_services/api/list-query";
|
|
8
|
+
import type { FilterValues } from "#core/_utils/filter";
|
|
8
9
|
import type { FiltersState } from "#core/hooks/use-filters";
|
|
9
10
|
import { useFilters } from "#core/hooks/use-filters";
|
|
10
11
|
|
|
11
12
|
export interface UseListQueryResult {
|
|
12
13
|
/** Pronta para o `.service.ts` da feature. */
|
|
13
14
|
query: ListQuery;
|
|
15
|
+
/**
|
|
16
|
+
* A `query` já vale — **o hook da feature só pode buscar depois disso**.
|
|
17
|
+
*
|
|
18
|
+
* Enquanto é `false`, o filtro da URL ainda não foi lido e a `query` sairia
|
|
19
|
+
* sem ele. Buscar assim mesmo faz duas requisições por carregamento, e a
|
|
20
|
+
* errada pode chegar por último: a tela abre mostrando tudo com o filtro
|
|
21
|
+
* marcado na barra.
|
|
22
|
+
*/
|
|
23
|
+
ready: boolean;
|
|
14
24
|
page: number;
|
|
15
25
|
pageSize: number;
|
|
16
26
|
sorting: SortingState;
|
|
@@ -30,8 +40,10 @@ export interface UseListQueryResult {
|
|
|
30
40
|
export function useListQuery(
|
|
31
41
|
schemaUrl: string,
|
|
32
42
|
initialPageSize = 10,
|
|
43
|
+
/** Filtro de quem chega sem `?filter=` — a listagem abre já filtrada. */
|
|
44
|
+
defaultFilterValues?: FilterValues,
|
|
33
45
|
): UseListQueryResult {
|
|
34
|
-
const filters = useFilters(schemaUrl);
|
|
46
|
+
const filters = useFilters(schemaUrl, defaultFilterValues);
|
|
35
47
|
const { filter } = filters;
|
|
36
48
|
|
|
37
49
|
const [page, setPage] = useState(0);
|
|
@@ -58,6 +70,7 @@ export function useListQuery(
|
|
|
58
70
|
|
|
59
71
|
return {
|
|
60
72
|
query,
|
|
73
|
+
ready: filters.hydrated,
|
|
61
74
|
page,
|
|
62
75
|
pageSize,
|
|
63
76
|
sorting,
|
package/src/hooks/use-request.ts
CHANGED
|
@@ -34,12 +34,24 @@ export interface RunOptions {
|
|
|
34
34
|
* "Registro criado" (ex.: "Link de primeiro acesso enviado").
|
|
35
35
|
*/
|
|
36
36
|
success?: RequestOperation | string;
|
|
37
|
+
/**
|
|
38
|
+
* Reage ao erro com o `errorCode` em mãos — para quando a tela precisa fazer
|
|
39
|
+
* algo além de mostrar a mensagem (ex.: voltar ao passo de credenciais
|
|
40
|
+
* quando a verificação de 2FA vence).
|
|
41
|
+
*
|
|
42
|
+
* Vem por callback e não pelo estado `errorCode` porque o estado só chega no
|
|
43
|
+
* render seguinte: quem lê logo depois do `await run(...)` pegaria o valor
|
|
44
|
+
* anterior.
|
|
45
|
+
*/
|
|
46
|
+
onError?: (error: { code: string | null; message: string }) => void;
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
export interface UseRequestResult {
|
|
40
50
|
run: <T>(fn: () => Promise<T>, options?: RunOptions) => Promise<T | null>;
|
|
41
51
|
loading: boolean;
|
|
42
52
|
error: string | null;
|
|
53
|
+
/** Código estável do último erro (`AppErrorCode` do backend), para render condicional. */
|
|
54
|
+
errorCode: string | null;
|
|
43
55
|
setError: Dispatch<SetStateAction<string | null>>;
|
|
44
56
|
}
|
|
45
57
|
|
|
@@ -53,6 +65,7 @@ export function useRequest(): UseRequestResult {
|
|
|
53
65
|
const { notify } = useToast();
|
|
54
66
|
const [loading, setLoading] = useState(false);
|
|
55
67
|
const [error, setError] = useState<string | null>(null);
|
|
68
|
+
const [errorCode, setErrorCode] = useState<string | null>(null);
|
|
56
69
|
|
|
57
70
|
/**
|
|
58
71
|
* Ordem de preferência da mensagem:
|
|
@@ -93,6 +106,7 @@ export function useRequest(): UseRequestResult {
|
|
|
93
106
|
async <T>(fn: () => Promise<T>, options?: RunOptions): Promise<T | null> => {
|
|
94
107
|
setLoading(true);
|
|
95
108
|
setError(null);
|
|
109
|
+
setErrorCode(null);
|
|
96
110
|
try {
|
|
97
111
|
const result = await fn();
|
|
98
112
|
if (options?.success) {
|
|
@@ -101,7 +115,12 @@ export function useRequest(): UseRequestResult {
|
|
|
101
115
|
return result;
|
|
102
116
|
} catch (e) {
|
|
103
117
|
const err = e as AxiosError<ApiErrorBody>;
|
|
104
|
-
|
|
118
|
+
const body = err.response?.data;
|
|
119
|
+
const message = messageFor(body);
|
|
120
|
+
const code = body?.errorCode ?? null;
|
|
121
|
+
setError(message);
|
|
122
|
+
setErrorCode(code);
|
|
123
|
+
options?.onError?.({ code, message });
|
|
105
124
|
return null;
|
|
106
125
|
} finally {
|
|
107
126
|
setLoading(false);
|
|
@@ -110,5 +129,5 @@ export function useRequest(): UseRequestResult {
|
|
|
110
129
|
[messageFor, notify, successMessage],
|
|
111
130
|
);
|
|
112
131
|
|
|
113
|
-
return { run, loading, error, setError };
|
|
132
|
+
return { run, loading, error, errorCode, setError };
|
|
114
133
|
}
|
package/src/i18n/messages/en.ts
CHANGED
|
@@ -13,11 +13,20 @@ export const en: Messages = {
|
|
|
13
13
|
audit: "Audit",
|
|
14
14
|
},
|
|
15
15
|
logs: {
|
|
16
|
+
outcomeSuccess: "Success",
|
|
17
|
+
outcomeError: "Error",
|
|
18
|
+
outcomeAborted: "Aborted",
|
|
19
|
+
errorValidation: "Validation",
|
|
20
|
+
errorAuth: "Authentication",
|
|
21
|
+
errorNotFound: "Not found",
|
|
22
|
+
errorRateLimit: "Rate limit",
|
|
23
|
+
errorDatabase: "Database",
|
|
24
|
+
errorIntegration: "Integration",
|
|
25
|
+
errorInternal: "Internal",
|
|
26
|
+
outcome: "Outcome",
|
|
16
27
|
title: "Logs",
|
|
17
28
|
subtitle:
|
|
18
|
-
"
|
|
19
|
-
tabRequests: "Requests",
|
|
20
|
-
tabErrors: "Errors",
|
|
29
|
+
"Errors, slow requests and auditable actions — route, status and duration.",
|
|
21
30
|
date: "Date/time",
|
|
22
31
|
route: "Route",
|
|
23
32
|
status: "Status",
|
|
@@ -29,10 +38,14 @@ export const en: Messages = {
|
|
|
29
38
|
type: "Type",
|
|
30
39
|
message: "Message",
|
|
31
40
|
viewDetails: "View details",
|
|
32
|
-
detailsTitle: "
|
|
41
|
+
detailsTitle: "Request details",
|
|
42
|
+
errorCode: "Error code",
|
|
43
|
+
userAgent: "User agent",
|
|
33
44
|
stackTrace: "Stack trace",
|
|
34
45
|
},
|
|
35
46
|
auditTrail: {
|
|
47
|
+
recordHistory: "Change history",
|
|
48
|
+
noRecordHistory: "This record has no changes yet.",
|
|
36
49
|
title: "Audit Trail",
|
|
37
50
|
subtitle: "Data change history — who changed what, and when.",
|
|
38
51
|
entity: "Entity",
|
|
@@ -87,7 +100,6 @@ export const en: Messages = {
|
|
|
87
100
|
hint: "Fill in only the fields you want to use. All of them are combined with AND.",
|
|
88
101
|
apply: "Apply",
|
|
89
102
|
clear: "Clear",
|
|
90
|
-
clearAll: "Clear all",
|
|
91
103
|
empty: "This listing has no filters yet.",
|
|
92
104
|
noOptions: "No options available",
|
|
93
105
|
all: "All",
|
|
@@ -200,6 +212,10 @@ export const en: Messages = {
|
|
|
200
212
|
loginSuccess: "Signed in successfully",
|
|
201
213
|
twoFactorConfigured: "2FA configured successfully",
|
|
202
214
|
codeSent: "Code sent to your email",
|
|
215
|
+
verificationExpired:
|
|
216
|
+
"Verification expired. Sign in again to continue — if you already scanned the QR code, it is still valid.",
|
|
217
|
+
expiresIn: "Time to confirm: {time}",
|
|
218
|
+
expiredHint: "Time is up. Sign in again to continue.",
|
|
203
219
|
},
|
|
204
220
|
recovery: {
|
|
205
221
|
forgotTitle: "Recover password",
|
|
@@ -240,6 +256,8 @@ export const en: Messages = {
|
|
|
240
256
|
phone: "Phone",
|
|
241
257
|
email: "Email",
|
|
242
258
|
roles: "Roles",
|
|
259
|
+
team: "Team",
|
|
260
|
+
selectTeam: "Select the team",
|
|
243
261
|
status: "Status",
|
|
244
262
|
active: "Active",
|
|
245
263
|
inactive: "Inactive",
|
|
@@ -273,13 +291,25 @@ export const en: Messages = {
|
|
|
273
291
|
avatarUpdated: "Profile photo updated",
|
|
274
292
|
avatarRemoved: "Profile photo removed",
|
|
275
293
|
},
|
|
276
|
-
rbac: {
|
|
294
|
+
rbac: {
|
|
295
|
+
title: "Roles & Permissions",
|
|
296
|
+
manager: "Manager",
|
|
297
|
+
noManager: "No manager",
|
|
298
|
+
},
|
|
277
299
|
validation: {
|
|
278
300
|
required: "Required field",
|
|
279
301
|
email: "Invalid email",
|
|
280
302
|
passwordStrong: "Min. 8 chars with uppercase, lowercase, number and symbol",
|
|
281
303
|
passwordsNotMatch: "Passwords do not match",
|
|
282
304
|
codeShort: "Code too short",
|
|
305
|
+
rules: {
|
|
306
|
+
strength: "Password strength",
|
|
307
|
+
minLength: "At least {count} characters",
|
|
308
|
+
lowercase: "At least one lowercase letter",
|
|
309
|
+
uppercase: "At least one uppercase letter",
|
|
310
|
+
digit: "At least one number",
|
|
311
|
+
special: "At least one special character (!@#$...)",
|
|
312
|
+
},
|
|
283
313
|
},
|
|
284
314
|
errors: {
|
|
285
315
|
unexpected: "Unexpected error",
|
|
@@ -291,6 +321,7 @@ export const en: Messages = {
|
|
|
291
321
|
PASSWORD_REUSED:
|
|
292
322
|
"This password has already been used. Choose one you have never used in this system.",
|
|
293
323
|
TOO_MANY_REQUESTS: "Too many attempts. Wait a moment and try again.",
|
|
324
|
+
PENDING_TOKEN_EXPIRED: "Verification expired. Sign in again to continue.",
|
|
294
325
|
},
|
|
295
326
|
},
|
|
296
327
|
};
|
package/src/i18n/messages/pt.ts
CHANGED
|
@@ -11,10 +11,20 @@ export const pt = {
|
|
|
11
11
|
audit: "Auditoria",
|
|
12
12
|
},
|
|
13
13
|
logs: {
|
|
14
|
+
outcomeSuccess: "Sucesso",
|
|
15
|
+
outcomeError: "Erro",
|
|
16
|
+
outcomeAborted: "Abortada",
|
|
17
|
+
errorValidation: "Validação",
|
|
18
|
+
errorAuth: "Autenticação",
|
|
19
|
+
errorNotFound: "Não encontrado",
|
|
20
|
+
errorRateLimit: "Limite de requisições",
|
|
21
|
+
errorDatabase: "Banco de dados",
|
|
22
|
+
errorIntegration: "Integração",
|
|
23
|
+
errorInternal: "Interno",
|
|
24
|
+
outcome: "Desfecho",
|
|
14
25
|
title: "Logs",
|
|
15
|
-
subtitle:
|
|
16
|
-
|
|
17
|
-
tabErrors: "Erros",
|
|
26
|
+
subtitle:
|
|
27
|
+
"Erros, requisições lentas e ações auditáveis — rota, status e duração.",
|
|
18
28
|
date: "Data/hora",
|
|
19
29
|
route: "Rota",
|
|
20
30
|
status: "Status",
|
|
@@ -26,10 +36,14 @@ export const pt = {
|
|
|
26
36
|
type: "Tipo",
|
|
27
37
|
message: "Mensagem",
|
|
28
38
|
viewDetails: "Ver detalhes",
|
|
29
|
-
detailsTitle: "Detalhes
|
|
39
|
+
detailsTitle: "Detalhes da requisição",
|
|
40
|
+
errorCode: "Código do erro",
|
|
41
|
+
userAgent: "User agent",
|
|
30
42
|
stackTrace: "Stack trace",
|
|
31
43
|
},
|
|
32
44
|
auditTrail: {
|
|
45
|
+
recordHistory: "Histórico de alterações",
|
|
46
|
+
noRecordHistory: "Este registro ainda não sofreu alterações.",
|
|
33
47
|
title: "Trilha de Auditoria",
|
|
34
48
|
subtitle: "Histórico de alterações de dados — quem mudou o quê, e quando.",
|
|
35
49
|
entity: "Entidade",
|
|
@@ -88,7 +102,6 @@ export const pt = {
|
|
|
88
102
|
hint: "Preencha só os campos que quiser usar. Tudo é combinado com E.",
|
|
89
103
|
apply: "Aplicar",
|
|
90
104
|
clear: "Limpar",
|
|
91
|
-
clearAll: "Limpar tudo",
|
|
92
105
|
empty: "Esta listagem ainda não tem filtros.",
|
|
93
106
|
noOptions: "Nenhuma opção disponível",
|
|
94
107
|
all: "Todos",
|
|
@@ -201,6 +214,10 @@ export const pt = {
|
|
|
201
214
|
loginSuccess: "Login realizado com sucesso",
|
|
202
215
|
twoFactorConfigured: "2FA configurado com sucesso",
|
|
203
216
|
codeSent: "Código enviado para seu email",
|
|
217
|
+
verificationExpired:
|
|
218
|
+
"A verificação expirou. Entre novamente para continuar — se você já escaneou o QR code, ele continua valendo.",
|
|
219
|
+
expiresIn: "Tempo para confirmar: {time}",
|
|
220
|
+
expiredHint: "O tempo acabou. Entre novamente para continuar.",
|
|
204
221
|
},
|
|
205
222
|
recovery: {
|
|
206
223
|
forgotTitle: "Recuperar senha",
|
|
@@ -241,6 +258,8 @@ export const pt = {
|
|
|
241
258
|
phone: "Telefone",
|
|
242
259
|
email: "Email",
|
|
243
260
|
roles: "Papéis",
|
|
261
|
+
team: "Equipe",
|
|
262
|
+
selectTeam: "Selecione a equipe",
|
|
244
263
|
status: "Status",
|
|
245
264
|
active: "Ativo",
|
|
246
265
|
inactive: "Inativo",
|
|
@@ -275,13 +294,27 @@ export const pt = {
|
|
|
275
294
|
avatarUpdated: "Foto de perfil atualizada",
|
|
276
295
|
avatarRemoved: "Foto de perfil removida",
|
|
277
296
|
},
|
|
278
|
-
rbac: {
|
|
297
|
+
rbac: {
|
|
298
|
+
title: "Papéis & Permissões",
|
|
299
|
+
manager: "Gerente",
|
|
300
|
+
noManager: "Sem gerente",
|
|
301
|
+
},
|
|
279
302
|
validation: {
|
|
280
303
|
required: "Campo obrigatório",
|
|
281
304
|
email: "Email inválido",
|
|
282
305
|
passwordStrong:
|
|
283
306
|
"Mín. 8 caracteres com maiúscula, minúscula, número e símbolo",
|
|
284
307
|
passwordsNotMatch: "As senhas não conferem",
|
|
308
|
+
// Uma linha por exigência — é o que o PasswordRequirements lista enquanto
|
|
309
|
+
// a pessoa digita. Devem descrever a mesma coisa que `passwordStrong`.
|
|
310
|
+
rules: {
|
|
311
|
+
strength: "Força da senha",
|
|
312
|
+
minLength: "No mínimo {count} caracteres",
|
|
313
|
+
lowercase: "Pelo menos uma letra minúscula",
|
|
314
|
+
uppercase: "Pelo menos uma letra maiúscula",
|
|
315
|
+
digit: "Pelo menos um número",
|
|
316
|
+
special: "Pelo menos um caractere especial (!@#$...)",
|
|
317
|
+
},
|
|
285
318
|
codeShort: "Código muito curto",
|
|
286
319
|
},
|
|
287
320
|
errors: {
|
|
@@ -298,6 +331,8 @@ export const pt = {
|
|
|
298
331
|
"Esta senha já foi utilizada. Escolha uma senha que você ainda não usou neste sistema.",
|
|
299
332
|
TOO_MANY_REQUESTS:
|
|
300
333
|
"Muitas tentativas em pouco tempo. Aguarde um instante e tente de novo.",
|
|
334
|
+
PENDING_TOKEN_EXPIRED:
|
|
335
|
+
"A verificação expirou. Entre novamente para continuar.",
|
|
301
336
|
},
|
|
302
337
|
},
|
|
303
338
|
};
|
package/src/index.ts
CHANGED
|
@@ -26,12 +26,24 @@ export { UsersScreen } from "#core/features/users/users-screen";
|
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
27
27
|
export * from "#core/components";
|
|
28
28
|
export * from "#core/contexts";
|
|
29
|
+
// Histórico de alterações de um registro, para a tela de edição do projeto.
|
|
30
|
+
export type {
|
|
31
|
+
AuditDiffProps,
|
|
32
|
+
RecordAuditButtonProps,
|
|
33
|
+
} from "#core/features/audit/components";
|
|
34
|
+
export {
|
|
35
|
+
AuditDiff,
|
|
36
|
+
RecordAuditButton,
|
|
37
|
+
} from "#core/features/audit/components";
|
|
29
38
|
|
|
30
39
|
// ---------------------------------------------------------------------------
|
|
31
40
|
// Primitivos de UI — agnósticos de domínio
|
|
32
41
|
// ---------------------------------------------------------------------------
|
|
33
42
|
export * from "#core/components/ui";
|
|
34
43
|
export { cn } from "#core/lib/utils";
|
|
44
|
+
// O projeto que criar tela com escopo de equipe monta o par de códigos com ele,
|
|
45
|
+
// em vez de repetir os sufixos à mão.
|
|
46
|
+
export { anyOrTeam } from "#core/_utils/permission";
|
|
35
47
|
|
|
36
48
|
// ---------------------------------------------------------------------------
|
|
37
49
|
// Hooks de listagem, requisição e filtro
|
|
@@ -47,7 +59,9 @@ export { useFilters } from "#core/hooks/use-filters";
|
|
|
47
59
|
export type { UseListQueryResult } from "#core/hooks/use-list-query";
|
|
48
60
|
export { useListQuery } from "#core/hooks/use-list-query";
|
|
49
61
|
export type { RunOptions, UseRequestResult } from "#core/hooks/use-request";
|
|
50
|
-
|
|
62
|
+
// RequestOperation é enum (valor, não só tipo): sem ele o projeto não consegue
|
|
63
|
+
// pedir o toast padrão de criado/alterado/removido que o core já traduz.
|
|
64
|
+
export { RequestOperation, useRequest } from "#core/hooks/use-request";
|
|
51
65
|
|
|
52
66
|
// ---------------------------------------------------------------------------
|
|
53
67
|
// HTTP e traduções
|
package/src/styles/core.css
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Design system do rl-core-front: tokens de cor, base do body,
|
|
3
|
-
* autofill e a barra de progresso do toast.
|
|
2
|
+
* Design system do rl-core-front: tipografia, tokens de cor, base do body,
|
|
3
|
+
* correção do autofill e a barra de progresso do toast.
|
|
4
4
|
*
|
|
5
5
|
* Um projeto que consome o pacote importa este arquivo no globals.css dele,
|
|
6
6
|
* depois do `@import "tailwindcss"`:
|
|
@@ -10,6 +10,44 @@
|
|
|
10
10
|
* @import "rl-core-front/styles.css";
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
/*
|
|
14
|
+
* Poppins, hospedada no próprio pacote — sem CDN, sem requisição a terceiro e
|
|
15
|
+
* sem passo de instalação no projeto: quem importa este arquivo já recebe a
|
|
16
|
+
* fonte. Só o subset `latin`, que cobre português e inglês por inteiro; glifo
|
|
17
|
+
* fora dele cai no fallback da stack, que é o comportamento normal do browser.
|
|
18
|
+
*
|
|
19
|
+
* Não usamos `next/font` aqui de propósito. O pacote é publicado em
|
|
20
|
+
* código-fonte, e `next/font` é resolvido pelo build de QUEM INSTALA — a mesma
|
|
21
|
+
* armadilha do alias `@/` descrita no §4 do CLAUDE.md.
|
|
22
|
+
*/
|
|
23
|
+
@font-face {
|
|
24
|
+
font-family: 'Poppins';
|
|
25
|
+
font-style: normal;
|
|
26
|
+
font-weight: 400;
|
|
27
|
+
font-display: swap;
|
|
28
|
+
src: url('./fonts/poppins-latin-400.woff2') format('woff2');
|
|
29
|
+
}
|
|
30
|
+
@font-face {
|
|
31
|
+
font-family: 'Poppins';
|
|
32
|
+
font-style: normal;
|
|
33
|
+
font-weight: 500;
|
|
34
|
+
font-display: swap;
|
|
35
|
+
src: url('./fonts/poppins-latin-500.woff2') format('woff2');
|
|
36
|
+
}
|
|
37
|
+
@font-face {
|
|
38
|
+
font-family: 'Poppins';
|
|
39
|
+
font-style: normal;
|
|
40
|
+
font-weight: 600;
|
|
41
|
+
font-display: swap;
|
|
42
|
+
src: url('./fonts/poppins-latin-600.woff2') format('woff2');
|
|
43
|
+
}
|
|
44
|
+
@font-face {
|
|
45
|
+
font-family: 'Poppins';
|
|
46
|
+
font-style: normal;
|
|
47
|
+
font-weight: 700;
|
|
48
|
+
font-display: swap;
|
|
49
|
+
src: url('./fonts/poppins-latin-700.woff2') format('woff2');
|
|
50
|
+
}
|
|
13
51
|
|
|
14
52
|
@layer base {
|
|
15
53
|
:root {
|
|
@@ -20,6 +58,20 @@
|
|
|
20
58
|
* escuro. É a causa raiz do input branco.
|
|
21
59
|
*/
|
|
22
60
|
color-scheme: light;
|
|
61
|
+
|
|
62
|
+
/*
|
|
63
|
+
* Tipografia — o ponto de extensão da fonte. O core entrega Poppins; um
|
|
64
|
+
* projeto troca a família inteira redefinindo esta variável no globals.css
|
|
65
|
+
* dele, inclusive apontando para o que o `next/font` gerou:
|
|
66
|
+
*
|
|
67
|
+
* :root { --font-sans: var(--font-geist), sans-serif; }
|
|
68
|
+
*
|
|
69
|
+
* Fica no `:root` e não é redeclarada no `.dark`: fonte não muda com tema.
|
|
70
|
+
*/
|
|
71
|
+
--font-sans: 'Poppins', ui-sans-serif, system-ui, -apple-system,
|
|
72
|
+
'Segoe UI', sans-serif;
|
|
73
|
+
--font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
|
|
74
|
+
|
|
23
75
|
--background: 210 20% 98%;
|
|
24
76
|
--foreground: 222 47% 11%;
|
|
25
77
|
--card: 0 0% 100%;
|
|
@@ -40,9 +92,32 @@
|
|
|
40
92
|
--success-foreground: 0 0% 100%;
|
|
41
93
|
--warning: 38 92% 50%;
|
|
42
94
|
--warning-foreground: 0 0% 100%;
|
|
95
|
+
|
|
96
|
+
/*
|
|
97
|
+
* Versão legível como TEXTO. Verde e âmbar são claros por natureza: no tema
|
|
98
|
+
* claro, `--success`/`--warning` escritos sobre fundo branco dão ~2:1 de
|
|
99
|
+
* contraste, abaixo do mínimo de acessibilidade. Preenchimento continua
|
|
100
|
+
* usando a cor cheia; quem vira letra usa esta.
|
|
101
|
+
*/
|
|
102
|
+
--success-strong: 142 72% 29%;
|
|
103
|
+
--warning-strong: 32 95% 30%;
|
|
43
104
|
--border: 214 32% 91%;
|
|
44
105
|
--input: 214 32% 91%;
|
|
45
106
|
--ring: 211 80% 42%;
|
|
107
|
+
|
|
108
|
+
/*
|
|
109
|
+
* Trilha e pílula acesa dos controles segmentados (`Tabs` e
|
|
110
|
+
* `SegmentedControl`, que dividem a mesma pele).
|
|
111
|
+
*
|
|
112
|
+
* Existem como par próprio porque a regra que precisa valer é relativa: a
|
|
113
|
+
* pílula acesa tem que ser mais clara que a trilha em que ela está. Antes
|
|
114
|
+
* isto era `bg-muted` + `bg-card`, e no tema escuro a relação se invertia
|
|
115
|
+
* (trilha 18%, acesa 13%) — a opção selecionada ficava mais ESCURA que o
|
|
116
|
+
* fundo dela e só se distinguia pelo peso da fonte.
|
|
117
|
+
*/
|
|
118
|
+
--track: 210 40% 94%;
|
|
119
|
+
--track-active: 0 0% 100%;
|
|
120
|
+
|
|
46
121
|
--radius: 0.6rem;
|
|
47
122
|
}
|
|
48
123
|
|
|
@@ -68,9 +143,17 @@
|
|
|
68
143
|
--success-foreground: 0 0% 100%;
|
|
69
144
|
--warning: 38 92% 55%;
|
|
70
145
|
--warning-foreground: 213 30% 10%;
|
|
146
|
+
|
|
147
|
+
/* No escuro o problema se inverte: como texto, precisam clarear. */
|
|
148
|
+
--success-strong: 142 60% 62%;
|
|
149
|
+
--warning-strong: 38 92% 65%;
|
|
71
150
|
--border: 213 20% 22%;
|
|
72
151
|
--input: 213 20% 24%;
|
|
73
152
|
--ring: 211 85% 60%;
|
|
153
|
+
|
|
154
|
+
/* Trilha abaixo do card (13%) e pílula acima dele: a acesa salta. */
|
|
155
|
+
--track: 213 25% 11%;
|
|
156
|
+
--track-active: 213 20% 24%;
|
|
74
157
|
}
|
|
75
158
|
}
|
|
76
159
|
|
|
@@ -80,7 +163,21 @@
|
|
|
80
163
|
}
|
|
81
164
|
body {
|
|
82
165
|
@apply bg-background text-foreground;
|
|
166
|
+
font-family: var(--font-sans);
|
|
83
167
|
font-feature-settings: 'rlig' 1, 'calt' 1;
|
|
168
|
+
-webkit-font-smoothing: antialiased;
|
|
169
|
+
-moz-osx-font-smoothing: grayscale;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/*
|
|
173
|
+
* Poppins é geométrica e larga: no tamanho de título, o espaçamento padrão
|
|
174
|
+
* abre demais. Fechar um pouco vale só do `xl` para cima — em texto corrido
|
|
175
|
+
* o mesmo ajuste prejudicaria a leitura.
|
|
176
|
+
*/
|
|
177
|
+
h1,
|
|
178
|
+
h2,
|
|
179
|
+
h3 {
|
|
180
|
+
letter-spacing: -0.02em;
|
|
84
181
|
}
|
|
85
182
|
|
|
86
183
|
/*
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/tailwind-preset.ts
CHANGED
|
@@ -33,6 +33,15 @@ const preset: Omit<Config, 'content'> = {
|
|
|
33
33
|
screens: { '2xl': '1400px' },
|
|
34
34
|
},
|
|
35
35
|
extend: {
|
|
36
|
+
/*
|
|
37
|
+
* Apontam para os tokens, e não para uma família fixa: é o que permite a
|
|
38
|
+
* um projeto trocar a fonte redefinindo `--font-sans` no CSS dele, sem
|
|
39
|
+
* tocar neste preset nem no pacote. Ver `styles/core.css`.
|
|
40
|
+
*/
|
|
41
|
+
fontFamily: {
|
|
42
|
+
sans: ['var(--font-sans)'],
|
|
43
|
+
mono: ['var(--font-mono)'],
|
|
44
|
+
},
|
|
36
45
|
colors: {
|
|
37
46
|
border: 'hsl(var(--border))',
|
|
38
47
|
input: 'hsl(var(--input))',
|
|
@@ -70,10 +79,18 @@ const preset: Omit<Config, 'content'> = {
|
|
|
70
79
|
success: {
|
|
71
80
|
DEFAULT: 'hsl(var(--success))',
|
|
72
81
|
foreground: 'hsl(var(--success-foreground))',
|
|
82
|
+
/* `strong` é a variante para texto — ver core.css. */
|
|
83
|
+
strong: 'hsl(var(--success-strong))',
|
|
73
84
|
},
|
|
74
85
|
warning: {
|
|
75
86
|
DEFAULT: 'hsl(var(--warning))',
|
|
76
87
|
foreground: 'hsl(var(--warning-foreground))',
|
|
88
|
+
strong: 'hsl(var(--warning-strong))',
|
|
89
|
+
},
|
|
90
|
+
/* Trilha e pílula acesa dos controles segmentados — ver core.css. */
|
|
91
|
+
track: {
|
|
92
|
+
DEFAULT: 'hsl(var(--track))',
|
|
93
|
+
active: 'hsl(var(--track-active))',
|
|
77
94
|
},
|
|
78
95
|
},
|
|
79
96
|
borderRadius: {
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { SlidersHorizontal, X } from "lucide-react";
|
|
4
|
-
import type { JSX } from "react";
|
|
5
|
-
|
|
6
|
-
import { countConditions } from "#core/_utils/advanced-filter";
|
|
7
|
-
import { describeValue, hasValue } from "#core/_utils/filter";
|
|
8
|
-
import { Button } from "#core/components/ui/button";
|
|
9
|
-
import { useFieldLabel } from "#core/components/ui/filter-sheet";
|
|
10
|
-
import { useI18n } from "#core/contexts";
|
|
11
|
-
import type { FiltersState } from "#core/hooks/use-filters";
|
|
12
|
-
|
|
13
|
-
export interface FilterChipsProps {
|
|
14
|
-
filters: FiltersState;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Filtros ativos em forma de chip, cada um removível.
|
|
19
|
-
*
|
|
20
|
-
* Existe para o usuário não precisar abrir o painel só para lembrar o que está
|
|
21
|
-
* filtrando — e para não olhar uma tabela vazia sem saber por quê. No modo
|
|
22
|
-
* avançado a expressão não cabe num chip por campo, então vira um resumo com a
|
|
23
|
-
* contagem de condições.
|
|
24
|
-
*/
|
|
25
|
-
export function FilterChips({ filters }: FilterChipsProps): JSX.Element | null {
|
|
26
|
-
const { t, locale } = useI18n();
|
|
27
|
-
const labelFor = useFieldLabel();
|
|
28
|
-
const { schema, values, mode, tree } = filters;
|
|
29
|
-
|
|
30
|
-
if (mode === "advanced") {
|
|
31
|
-
const total = tree ? countConditions(tree) : 0;
|
|
32
|
-
if (total === 0 || !filters.filter) {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
return (
|
|
36
|
-
<div className="mb-3 flex flex-wrap items-center gap-2">
|
|
37
|
-
<span className="flex items-center gap-1.5 rounded-full border border-border bg-muted/40 px-3 py-1 text-xs">
|
|
38
|
-
<SlidersHorizontal className="h-3 w-3" />
|
|
39
|
-
{t("filters.advancedSummary", { count: total })}
|
|
40
|
-
</span>
|
|
41
|
-
<Button variant="ghost" size="sm" onClick={() => filters.setMode("simple")}>
|
|
42
|
-
{t("filters.clearAll")}
|
|
43
|
-
</Button>
|
|
44
|
-
</div>
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const active = schema.filter((field) => hasValue(values[field.field]));
|
|
49
|
-
if (active.length === 0) {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const remove = (field: string): void => {
|
|
54
|
-
const next = { ...values };
|
|
55
|
-
delete next[field];
|
|
56
|
-
filters.setValues(next);
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const labels = {
|
|
60
|
-
from: t("filters.fromChip"),
|
|
61
|
-
to: t("filters.toChip"),
|
|
62
|
-
yes: t("common.yes"),
|
|
63
|
-
no: t("common.no"),
|
|
64
|
-
locale,
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
<div className="mb-3 flex flex-wrap items-center gap-2">
|
|
69
|
-
{active.map((field) => (
|
|
70
|
-
<button
|
|
71
|
-
key={field.field}
|
|
72
|
-
type="button"
|
|
73
|
-
onClick={() => remove(field.field)}
|
|
74
|
-
className="flex items-center gap-1.5 rounded-full border border-border bg-muted/40 px-3 py-1 text-xs transition-colors hover:bg-muted"
|
|
75
|
-
>
|
|
76
|
-
<span className="text-muted-foreground">{labelFor(field)}:</span>
|
|
77
|
-
<span className="font-medium">
|
|
78
|
-
{describeValue(field, values[field.field], labels)}
|
|
79
|
-
</span>
|
|
80
|
-
<X className="h-3 w-3" />
|
|
81
|
-
</button>
|
|
82
|
-
))}
|
|
83
|
-
<Button variant="ghost" size="sm" onClick={() => filters.setValues({})}>
|
|
84
|
-
{t("filters.clearAll")}
|
|
85
|
-
</Button>
|
|
86
|
-
</div>
|
|
87
|
-
);
|
|
88
|
-
}
|