rl-core-front 0.4.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 +43 -0
- package/src/_services/auth/index.ts +2 -0
- 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 +37 -13
- 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/index.ts +2 -0
- package/src/components/ui/password-requirements.tsx +105 -0
- package/src/components/ui/segmented-control.tsx +7 -6
- 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/color-mode-context.tsx +4 -2
- package/src/contexts/i18n-context.tsx +3 -2
- package/src/contexts/socket-context.tsx +27 -1
- package/src/features/audit/components/audit-diff.tsx +1 -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 +17 -4
- package/src/features/profile/force-password-change.tsx +18 -3
- package/src/features/profile/profile-screen.tsx +8 -1
- package/src/features/rbac/panels/groups-panel.tsx +58 -23
- 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 +49 -2
- package/src/features/users/hooks/use-users.ts +15 -1
- 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-request.ts +21 -2
- package/src/i18n/messages/en.ts +20 -1
- package/src/i18n/messages/pt.ts +23 -1
- package/src/index.ts +3 -0
- 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
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useState } from "react";
|
|
4
4
|
|
|
5
|
+
import { safeStorage } from "#core/_utils/storage";
|
|
6
|
+
|
|
5
7
|
const STORAGE_PREFIX = "cols:";
|
|
6
8
|
|
|
7
9
|
export interface UseColumnVisibilityResult {
|
|
@@ -20,10 +22,13 @@ export function useColumnVisibility(
|
|
|
20
22
|
const [hiddenColumns, setHiddenColumns] = useState<string[]>([]);
|
|
21
23
|
|
|
22
24
|
useEffect(() => {
|
|
23
|
-
if (!storageKey
|
|
25
|
+
if (!storageKey) {return;}
|
|
26
|
+
const raw = safeStorage.get(`${STORAGE_PREFIX}${storageKey}`);
|
|
27
|
+
if (!raw) {return;}
|
|
24
28
|
try {
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
// O `try` que sobrou é do JSON: o acesso ao armazenamento já é seguro,
|
|
30
|
+
// mas o conteúdo pode estar corrompido por uma versão anterior da tela.
|
|
31
|
+
setHiddenColumns(JSON.parse(raw));
|
|
27
32
|
} catch {
|
|
28
33
|
setHiddenColumns([]);
|
|
29
34
|
}
|
|
@@ -37,15 +42,11 @@ export function useColumnVisibility(
|
|
|
37
42
|
const saveHiddenColumns = useCallback(
|
|
38
43
|
(nextHidden: string[]) => {
|
|
39
44
|
setHiddenColumns(nextHidden);
|
|
40
|
-
if (!storageKey
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
);
|
|
46
|
-
} catch {
|
|
47
|
-
/* ignore */
|
|
48
|
-
}
|
|
45
|
+
if (!storageKey) {return;}
|
|
46
|
+
safeStorage.set(
|
|
47
|
+
`${STORAGE_PREFIX}${storageKey}`,
|
|
48
|
+
JSON.stringify(nextHidden),
|
|
49
|
+
);
|
|
49
50
|
},
|
|
50
51
|
[storageKey],
|
|
51
52
|
);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
export interface UseCountdownResult {
|
|
6
|
+
/** Segundos restantes, nunca negativo. */
|
|
7
|
+
remaining: number;
|
|
8
|
+
/** `mm:ss` pronto para exibir. */
|
|
9
|
+
formatted: string;
|
|
10
|
+
/** Chegou a zero. */
|
|
11
|
+
expired: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const format = (totalSeconds: number): string => {
|
|
15
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
16
|
+
const seconds = totalSeconds % 60;
|
|
17
|
+
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Contagem regressiva em segundos.
|
|
22
|
+
*
|
|
23
|
+
* Conta pelo relógio, não somando ticks: um intervalo de 1s atrasa quando a aba
|
|
24
|
+
* fica em segundo plano, e o tempo iria derretendo. O instante-alvo é fixado
|
|
25
|
+
* uma vez, dentro do efeito, e cada tique só relê a diferença — voltar para a
|
|
26
|
+
* aba mostra o tempo real, não o que o timer conseguiu contar.
|
|
27
|
+
*
|
|
28
|
+
* `seconds` nulo (prazo ainda desconhecido) deixa o contador parado em zero sem
|
|
29
|
+
* marcar como expirado.
|
|
30
|
+
*
|
|
31
|
+
* O relógio é lido só no efeito e no callback do intervalo: `Date.now()` é
|
|
32
|
+
* impuro e durante o render produziria resultado instável a cada re-render.
|
|
33
|
+
*/
|
|
34
|
+
export function useCountdown(seconds: number | null): UseCountdownResult {
|
|
35
|
+
/*
|
|
36
|
+
* Guarda de que prazo veio a contagem. Sem isso, trocar `seconds` deixaria o
|
|
37
|
+
* valor do prazo anterior à mostra até o primeiro tique — e um zero herdado
|
|
38
|
+
* marcaria como expirado um contador que acabou de começar.
|
|
39
|
+
*/
|
|
40
|
+
const [tick, setTick] = useState<{ source: number | null; value: number }>({
|
|
41
|
+
source: seconds,
|
|
42
|
+
value: seconds ?? 0,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (seconds === null) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const deadline = Date.now() + seconds * 1000;
|
|
50
|
+
const id = setInterval(
|
|
51
|
+
() =>
|
|
52
|
+
setTick({
|
|
53
|
+
source: seconds,
|
|
54
|
+
value: Math.max(0, Math.round((deadline - Date.now()) / 1000)),
|
|
55
|
+
}),
|
|
56
|
+
1000,
|
|
57
|
+
);
|
|
58
|
+
return () => clearInterval(id);
|
|
59
|
+
}, [seconds]);
|
|
60
|
+
|
|
61
|
+
// Enquanto o tique ainda é do prazo anterior, vale o prazo cheio recebido.
|
|
62
|
+
const remaining =
|
|
63
|
+
seconds === null ? 0 : tick.source === seconds ? tick.value : seconds;
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
remaining,
|
|
67
|
+
formatted: format(remaining),
|
|
68
|
+
expired: seconds !== null && remaining === 0,
|
|
69
|
+
};
|
|
70
|
+
}
|
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
|
@@ -212,6 +212,10 @@ export const en: Messages = {
|
|
|
212
212
|
loginSuccess: "Signed in successfully",
|
|
213
213
|
twoFactorConfigured: "2FA configured successfully",
|
|
214
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.",
|
|
215
219
|
},
|
|
216
220
|
recovery: {
|
|
217
221
|
forgotTitle: "Recover password",
|
|
@@ -252,6 +256,8 @@ export const en: Messages = {
|
|
|
252
256
|
phone: "Phone",
|
|
253
257
|
email: "Email",
|
|
254
258
|
roles: "Roles",
|
|
259
|
+
team: "Team",
|
|
260
|
+
selectTeam: "Select the team",
|
|
255
261
|
status: "Status",
|
|
256
262
|
active: "Active",
|
|
257
263
|
inactive: "Inactive",
|
|
@@ -285,13 +291,25 @@ export const en: Messages = {
|
|
|
285
291
|
avatarUpdated: "Profile photo updated",
|
|
286
292
|
avatarRemoved: "Profile photo removed",
|
|
287
293
|
},
|
|
288
|
-
rbac: {
|
|
294
|
+
rbac: {
|
|
295
|
+
title: "Roles & Permissions",
|
|
296
|
+
manager: "Manager",
|
|
297
|
+
noManager: "No manager",
|
|
298
|
+
},
|
|
289
299
|
validation: {
|
|
290
300
|
required: "Required field",
|
|
291
301
|
email: "Invalid email",
|
|
292
302
|
passwordStrong: "Min. 8 chars with uppercase, lowercase, number and symbol",
|
|
293
303
|
passwordsNotMatch: "Passwords do not match",
|
|
294
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
|
+
},
|
|
295
313
|
},
|
|
296
314
|
errors: {
|
|
297
315
|
unexpected: "Unexpected error",
|
|
@@ -303,6 +321,7 @@ export const en: Messages = {
|
|
|
303
321
|
PASSWORD_REUSED:
|
|
304
322
|
"This password has already been used. Choose one you have never used in this system.",
|
|
305
323
|
TOO_MANY_REQUESTS: "Too many attempts. Wait a moment and try again.",
|
|
324
|
+
PENDING_TOKEN_EXPIRED: "Verification expired. Sign in again to continue.",
|
|
306
325
|
},
|
|
307
326
|
},
|
|
308
327
|
};
|
package/src/i18n/messages/pt.ts
CHANGED
|
@@ -214,6 +214,10 @@ export const pt = {
|
|
|
214
214
|
loginSuccess: "Login realizado com sucesso",
|
|
215
215
|
twoFactorConfigured: "2FA configurado com sucesso",
|
|
216
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.",
|
|
217
221
|
},
|
|
218
222
|
recovery: {
|
|
219
223
|
forgotTitle: "Recuperar senha",
|
|
@@ -254,6 +258,8 @@ export const pt = {
|
|
|
254
258
|
phone: "Telefone",
|
|
255
259
|
email: "Email",
|
|
256
260
|
roles: "Papéis",
|
|
261
|
+
team: "Equipe",
|
|
262
|
+
selectTeam: "Selecione a equipe",
|
|
257
263
|
status: "Status",
|
|
258
264
|
active: "Ativo",
|
|
259
265
|
inactive: "Inativo",
|
|
@@ -288,13 +294,27 @@ export const pt = {
|
|
|
288
294
|
avatarUpdated: "Foto de perfil atualizada",
|
|
289
295
|
avatarRemoved: "Foto de perfil removida",
|
|
290
296
|
},
|
|
291
|
-
rbac: {
|
|
297
|
+
rbac: {
|
|
298
|
+
title: "Papéis & Permissões",
|
|
299
|
+
manager: "Gerente",
|
|
300
|
+
noManager: "Sem gerente",
|
|
301
|
+
},
|
|
292
302
|
validation: {
|
|
293
303
|
required: "Campo obrigatório",
|
|
294
304
|
email: "Email inválido",
|
|
295
305
|
passwordStrong:
|
|
296
306
|
"Mín. 8 caracteres com maiúscula, minúscula, número e símbolo",
|
|
297
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
|
+
},
|
|
298
318
|
codeShort: "Código muito curto",
|
|
299
319
|
},
|
|
300
320
|
errors: {
|
|
@@ -311,6 +331,8 @@ export const pt = {
|
|
|
311
331
|
"Esta senha já foi utilizada. Escolha uma senha que você ainda não usou neste sistema.",
|
|
312
332
|
TOO_MANY_REQUESTS:
|
|
313
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.",
|
|
314
336
|
},
|
|
315
337
|
},
|
|
316
338
|
};
|
package/src/index.ts
CHANGED
|
@@ -41,6 +41,9 @@ export {
|
|
|
41
41
|
// ---------------------------------------------------------------------------
|
|
42
42
|
export * from "#core/components/ui";
|
|
43
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";
|
|
44
47
|
|
|
45
48
|
// ---------------------------------------------------------------------------
|
|
46
49
|
// Hooks de listagem, requisição e filtro
|
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: {
|