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.
Files changed (80) hide show
  1. package/package.json +1 -1
  2. package/src/_services/api/app-error-code.enum.ts +23 -0
  3. package/src/_services/api/schema.d.ts +86 -132
  4. package/src/_services/auth/index.ts +2 -0
  5. package/src/_utils/filter.ts +15 -34
  6. package/src/_utils/password.ts +40 -0
  7. package/src/_utils/permission.ts +14 -0
  8. package/src/_utils/storage.ts +64 -0
  9. package/src/_utils/user.ts +19 -0
  10. package/src/components/app-shell.tsx +85 -11
  11. package/src/components/brand-header.tsx +23 -8
  12. package/src/components/brand-panel.tsx +41 -0
  13. package/src/components/index.ts +1 -0
  14. package/src/components/password-field.tsx +49 -22
  15. package/src/components/ui/alert.tsx +34 -5
  16. package/src/components/ui/badge.tsx +2 -2
  17. package/src/components/ui/button.tsx +5 -0
  18. package/src/components/ui/confirm-dialog.tsx +3 -1
  19. package/src/components/ui/data-table.tsx +13 -4
  20. package/src/components/ui/dialog.tsx +120 -21
  21. package/src/components/ui/filter-sheet.tsx +47 -25
  22. package/src/components/ui/index.ts +3 -1
  23. package/src/components/ui/password-requirements.tsx +105 -0
  24. package/src/components/ui/segmented-control.tsx +75 -0
  25. package/src/components/ui/simple-select.tsx +74 -0
  26. package/src/components/ui/switch.tsx +3 -1
  27. package/src/components/ui/tabs.tsx +4 -8
  28. package/src/components/ui/track.styles.ts +31 -0
  29. package/src/contexts/brand-context.tsx +15 -0
  30. package/src/contexts/color-mode-context.tsx +4 -2
  31. package/src/contexts/i18n-context.tsx +72 -6
  32. package/src/contexts/socket-context.tsx +27 -1
  33. package/src/features/audit/audit-screen.tsx +13 -140
  34. package/src/features/audit/components/audit-diff.tsx +156 -0
  35. package/src/features/audit/components/index.ts +3 -0
  36. package/src/features/audit/components/record-audit-button.tsx +176 -0
  37. package/src/features/audit/enums/audit-data-action.enum.ts +20 -0
  38. package/src/features/audit/hooks/use-audit-trail.ts +9 -2
  39. package/src/features/audit/services/audit.service.ts +43 -1
  40. package/src/features/login/components/backup-codes-dialog.tsx +4 -4
  41. package/src/features/login/components/login-form.tsx +18 -3
  42. package/src/features/login/components/two-factor-setup.tsx +32 -2
  43. package/src/features/login/hooks/use-login-flow.ts +60 -7
  44. package/src/features/login/login-screen.tsx +99 -40
  45. package/src/features/logs/enums/error-type.enum.ts +21 -0
  46. package/src/features/logs/enums/request-outcome.enum.ts +14 -0
  47. package/src/features/logs/hooks/use-request-logs.ts +26 -3
  48. package/src/features/logs/logs-screen.tsx +2 -14
  49. package/src/features/logs/panels/index.ts +0 -1
  50. package/src/features/logs/panels/request-logs-panel.tsx +177 -25
  51. package/src/features/logs/services/logs.service.ts +13 -51
  52. package/src/features/notifications/components/notification-item.tsx +10 -8
  53. package/src/features/notifications/enums/notification-type.enum.ts +8 -0
  54. package/src/features/profile/force-password-change.tsx +18 -3
  55. package/src/features/profile/profile-screen.tsx +10 -2
  56. package/src/features/rbac/panels/groups-panel.tsx +78 -30
  57. package/src/features/rbac/panels/roles-panel.tsx +2 -2
  58. package/src/features/rbac/services/rbac.service.ts +23 -27
  59. package/src/features/recovery/reset-password-screen.tsx +16 -2
  60. package/src/features/users/components/user-form-dialog.tsx +65 -9
  61. package/src/features/users/hooks/use-users.ts +24 -3
  62. package/src/features/users/services/users.service.ts +16 -0
  63. package/src/features/users/users-screen.tsx +17 -12
  64. package/src/hooks/use-column-visibility.ts +13 -12
  65. package/src/hooks/use-countdown.ts +70 -0
  66. package/src/hooks/use-filters.ts +25 -2
  67. package/src/hooks/use-list-query.ts +14 -1
  68. package/src/hooks/use-request.ts +21 -2
  69. package/src/i18n/messages/en.ts +37 -6
  70. package/src/i18n/messages/pt.ts +41 -6
  71. package/src/index.ts +15 -1
  72. package/src/styles/core.css +99 -2
  73. package/src/styles/fonts/poppins-latin-400.woff2 +0 -0
  74. package/src/styles/fonts/poppins-latin-500.woff2 +0 -0
  75. package/src/styles/fonts/poppins-latin-600.woff2 +0 -0
  76. package/src/styles/fonts/poppins-latin-700.woff2 +0 -0
  77. package/tailwind-preset.ts +17 -0
  78. package/src/components/ui/filter-chips.tsx +0 -88
  79. package/src/features/logs/hooks/use-error-logs.ts +0 -65
  80. 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,
@@ -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
- setError(messageFor(err.response?.data));
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
  }
@@ -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
- "Every API request success and error, route, status and duration.",
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: "Error details",
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: { title: "Roles & Permissions" },
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
  };
@@ -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: "Toda requisição à API — sucesso e erro, rota, status e duração.",
16
- tabRequests: "Requisições",
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 do erro",
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: { title: "Papéis & Permissões" },
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
- export { useRequest } from "#core/hooks/use-request";
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
@@ -1,6 +1,6 @@
1
1
  /*
2
- * Design system do rl-core-front: tokens de cor, base do body, correção do
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
  /*
@@ -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
- }