rl-core-front 0.16.0 → 0.16.1
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/components/ui/data-table.tsx +113 -2
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/tab-shortcuts.tsx +77 -0
- package/src/features/login/hooks/use-login-flow.ts +1 -1
- package/src/features/notifications/components/notification-item.tsx +34 -10
- package/src/features/notifications/hooks/use-notifications.ts +32 -1
- package/src/features/notifications/notifications-center.tsx +37 -15
- package/src/features/notifications/services/notifications.service.ts +10 -0
- package/src/features/profile/force-password-change.tsx +1 -1
- package/src/features/profile/profile-screen.tsx +4 -2
- package/src/features/recovery/reset-password-screen.tsx +1 -1
- package/src/features/shortcuts/components/shortcut-card.tsx +0 -5
- package/src/features/shortcuts/shortcuts-screen.tsx +14 -3
- package/src/hooks/use-request.ts +47 -5
- package/src/hooks/use-shortcut.ts +52 -3
- package/src/i18n/messages/en.ts +11 -1
- package/src/i18n/messages/pt.ts +11 -1
- package/src/index.ts +3 -0
package/package.json
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
PaginationState,
|
|
12
12
|
RowData,
|
|
13
13
|
rowPaginationFeature,
|
|
14
|
+
rowSelectionFeature,
|
|
15
|
+
RowSelectionState,
|
|
14
16
|
rowSortingFeature,
|
|
15
17
|
sortFn_alphanumeric,
|
|
16
18
|
sortFn_basic,
|
|
@@ -30,13 +32,14 @@ import {
|
|
|
30
32
|
SlidersHorizontal,
|
|
31
33
|
} from "lucide-react";
|
|
32
34
|
import type { JSX, PointerEvent as ReactPointerEvent } from "react";
|
|
33
|
-
import { Fragment, useMemo, useRef, useState } from "react";
|
|
35
|
+
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
|
|
34
36
|
|
|
35
37
|
import { countConditions } from "#core/_utils/advanced-filter";
|
|
36
38
|
import { activeFilterCount } from "#core/_utils/filter";
|
|
37
39
|
import { pageRange } from "#core/_utils/page-range";
|
|
38
40
|
import { Button } from "#core/components/ui/button";
|
|
39
41
|
import { Card } from "#core/components/ui/card";
|
|
42
|
+
import { Checkbox } from "#core/components/ui/checkbox";
|
|
40
43
|
import {
|
|
41
44
|
ColumnVisibilityModal,
|
|
42
45
|
ColumnVisibilityOption,
|
|
@@ -62,6 +65,7 @@ import { cn } from "#core/lib/utils";
|
|
|
62
65
|
|
|
63
66
|
export const dataTableFeatures = tableFeatures({
|
|
64
67
|
columnVisibilityFeature,
|
|
68
|
+
rowSelectionFeature,
|
|
65
69
|
rowSortingFeature,
|
|
66
70
|
rowPaginationFeature,
|
|
67
71
|
sortedRowModel: createSortedRowModel(),
|
|
@@ -75,6 +79,48 @@ export const dataTableFeatures = tableFeatures({
|
|
|
75
79
|
|
|
76
80
|
export type DataTableFeatures = typeof dataTableFeatures;
|
|
77
81
|
|
|
82
|
+
/**
|
|
83
|
+
* A coluna das caixas de seleção.
|
|
84
|
+
*
|
|
85
|
+
* Fora do componente porque ela não depende de nada dele além dos dois rótulos
|
|
86
|
+
* — e defini-la dentro faria uma coluna nova a cada render, que é o suficiente
|
|
87
|
+
* para o TanStack remontar a tabela e perder a seleção no meio do clique.
|
|
88
|
+
*
|
|
89
|
+
* A caixa do cabeçalho marca **a página**, e não a base: com paginação no
|
|
90
|
+
* servidor, "todas" seria uma promessa que a tabela não tem como cumprir — ela
|
|
91
|
+
* só tem em mãos a página atual.
|
|
92
|
+
*/
|
|
93
|
+
const selectionColumn = <T extends RowData>(
|
|
94
|
+
selectAllLabel: string,
|
|
95
|
+
selectRowLabel: string,
|
|
96
|
+
): ColumnDef<DataTableFeatures, T, unknown> => ({
|
|
97
|
+
id: "select",
|
|
98
|
+
meta: { shrink: true },
|
|
99
|
+
enableHiding: false,
|
|
100
|
+
enableSorting: false,
|
|
101
|
+
header: ({ table }) => (
|
|
102
|
+
<Checkbox
|
|
103
|
+
aria-label={selectAllLabel}
|
|
104
|
+
checked={
|
|
105
|
+
table.getIsAllPageRowsSelected()
|
|
106
|
+
? true
|
|
107
|
+
: table.getIsSomePageRowsSelected()
|
|
108
|
+
? "indeterminate"
|
|
109
|
+
: false
|
|
110
|
+
}
|
|
111
|
+
onCheckedChange={(marcado) => table.toggleAllPageRowsSelected(marcado === true)}
|
|
112
|
+
/>
|
|
113
|
+
),
|
|
114
|
+
cell: ({ row }) => (
|
|
115
|
+
<Checkbox
|
|
116
|
+
aria-label={selectRowLabel}
|
|
117
|
+
checked={row.getIsSelected()}
|
|
118
|
+
disabled={!row.getCanSelect()}
|
|
119
|
+
onCheckedChange={(marcado) => row.toggleSelected(marcado === true)}
|
|
120
|
+
/>
|
|
121
|
+
),
|
|
122
|
+
});
|
|
123
|
+
|
|
78
124
|
interface DataTableProps<T extends RowData> {
|
|
79
125
|
columns: ColumnDef<DataTableFeatures, T, unknown>[];
|
|
80
126
|
data: T[];
|
|
@@ -103,6 +149,23 @@ interface DataTableProps<T extends RowData> {
|
|
|
103
149
|
onSortingChange?: OnChangeFn<SortingState>;
|
|
104
150
|
/** Habilita ordenação (padrão true). Desligue quando o backend ainda não ordena. */
|
|
105
151
|
enableSorting?: boolean;
|
|
152
|
+
/**
|
|
153
|
+
* Liga a seleção de linhas — uma coluna de caixas antes das outras.
|
|
154
|
+
*
|
|
155
|
+
* A função existe para a linha que **não** pode entrar: numa fatura, a
|
|
156
|
+
* parcela já paga não se paga de novo. Desabilitar a caixa é melhor que
|
|
157
|
+
* escondê-la, porque a coluna continua alinhada e a linha continua contando
|
|
158
|
+
* como linha.
|
|
159
|
+
*/
|
|
160
|
+
enableRowSelection?: boolean | ((row: T) => boolean);
|
|
161
|
+
/**
|
|
162
|
+
* O que está selecionado, a cada mudança.
|
|
163
|
+
*
|
|
164
|
+
* O `clear` vem junto de propósito: quem age sobre a seleção precisa desfazê-la
|
|
165
|
+
* depois — pagou as seis parcelas, as caixas têm de esvaziar. Guardar a função
|
|
166
|
+
* é mais simples que a tabela adivinhar quando a ação terminou.
|
|
167
|
+
*/
|
|
168
|
+
onSelectionChange?: (rows: T[], clear: () => void) => void;
|
|
106
169
|
/**
|
|
107
170
|
* Filtros dinâmicos, como o `useListQuery` os devolve. Sem esta prop o botão
|
|
108
171
|
* não aparece — listagem sem catálogo continua funcionando igual.
|
|
@@ -372,6 +435,8 @@ export function DataTable<T extends RowData>({
|
|
|
372
435
|
sorting,
|
|
373
436
|
onSortingChange,
|
|
374
437
|
enableSorting = true,
|
|
438
|
+
enableRowSelection,
|
|
439
|
+
onSelectionChange,
|
|
375
440
|
filters,
|
|
376
441
|
hiddenFilterFields,
|
|
377
442
|
emptyMessage,
|
|
@@ -453,15 +518,34 @@ export function DataTable<T extends RowData>({
|
|
|
453
518
|
? { pageIndex: 0, pageSize: Math.max(data.length, 1) }
|
|
454
519
|
: internalPagination;
|
|
455
520
|
|
|
521
|
+
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
|
522
|
+
|
|
523
|
+
/*
|
|
524
|
+
A coluna das caixas entra na frente das outras, e só existe com a seleção
|
|
525
|
+
ligada: uma coluna a mais em toda tabela do sistema custaria 40px de largura
|
|
526
|
+
a telas que nunca vão selecionar nada.
|
|
527
|
+
|
|
528
|
+
`enableHiding: false` porque ela não é dado — esconder a caixa deixaria a
|
|
529
|
+
seleção ativa e invisível, que é o pior estado possível.
|
|
530
|
+
*/
|
|
531
|
+
const colunas = useMemo(
|
|
532
|
+
() =>
|
|
533
|
+
enableRowSelection
|
|
534
|
+
? [selectionColumn<T>(t("table.selectAll"), t("table.selectRow")), ...columns]
|
|
535
|
+
: columns,
|
|
536
|
+
[columns, enableRowSelection, t],
|
|
537
|
+
);
|
|
538
|
+
|
|
456
539
|
const table = useTable({
|
|
457
540
|
features: dataTableFeatures,
|
|
458
541
|
data,
|
|
459
|
-
columns,
|
|
542
|
+
columns: colunas,
|
|
460
543
|
getRowId: getRowId ? (row) => getRowId(row) : undefined,
|
|
461
544
|
state: {
|
|
462
545
|
columnVisibility,
|
|
463
546
|
sorting: manualSorting ? sorting : internalSorting,
|
|
464
547
|
pagination: manualPagination ? pagination : paginacaoLocal,
|
|
548
|
+
rowSelection,
|
|
465
549
|
},
|
|
466
550
|
onColumnVisibilityChange: (updater) => {
|
|
467
551
|
const next =
|
|
@@ -472,6 +556,11 @@ export function DataTable<T extends RowData>({
|
|
|
472
556
|
.map(([k]) => k),
|
|
473
557
|
);
|
|
474
558
|
},
|
|
559
|
+
onRowSelectionChange: setRowSelection,
|
|
560
|
+
enableRowSelection:
|
|
561
|
+
typeof enableRowSelection === "function"
|
|
562
|
+
? (row: { original: T }) => enableRowSelection(row.original)
|
|
563
|
+
: enableRowSelection,
|
|
475
564
|
onSortingChange: manualSorting ? onSortingChange : setInternalSorting,
|
|
476
565
|
onPaginationChange: manualPagination
|
|
477
566
|
? onPaginationChange
|
|
@@ -482,6 +571,28 @@ export function DataTable<T extends RowData>({
|
|
|
482
571
|
...(manualPagination ? { rowCount: rowCount ?? 0 } : {}),
|
|
483
572
|
});
|
|
484
573
|
|
|
574
|
+
/*
|
|
575
|
+
A tela é avisada num efeito, e não dentro do `onRowSelectionChange`.
|
|
576
|
+
|
|
577
|
+
O handler roda **antes** de o estado entrar, então ali a tabela ainda
|
|
578
|
+
responde a seleção antiga — e a tela receberia sempre uma seleção atrasada
|
|
579
|
+
em um clique, que é o tipo de erro que só aparece quando alguém confere o
|
|
580
|
+
total do que selecionou.
|
|
581
|
+
*/
|
|
582
|
+
const selecionadas = table.getSelectedRowModel().rows.map((row) => row.original);
|
|
583
|
+
const avisar = useRef(onSelectionChange);
|
|
584
|
+
|
|
585
|
+
useEffect(() => {
|
|
586
|
+
avisar.current = onSelectionChange;
|
|
587
|
+
}, [onSelectionChange]);
|
|
588
|
+
|
|
589
|
+
useEffect(() => {
|
|
590
|
+
avisar.current?.(selecionadas as T[], () => setRowSelection({}));
|
|
591
|
+
// A dependência é o **estado** da seleção, e não a lista derivada: esta é
|
|
592
|
+
// um array novo a cada render, e o efeito rodaria sempre.
|
|
593
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
594
|
+
}, [rowSelection]);
|
|
595
|
+
|
|
485
596
|
/*
|
|
486
597
|
Linha de fechamento: existe só se alguma coluna visível declarar `footer`.
|
|
487
598
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { JSX } from "react";
|
|
4
|
+
|
|
5
|
+
import { MAX_TAB_SHORTCUTS, useShortcut } from "#core/hooks/use-shortcut";
|
|
6
|
+
|
|
7
|
+
export interface TabShortcutsProps {
|
|
8
|
+
/** Os valores das abas, na ordem em que aparecem. */
|
|
9
|
+
values: readonly string[];
|
|
10
|
+
onSelect: (value: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* O rótulo do atalho de uma aba, para a dica que a própria aba mostra.
|
|
15
|
+
*
|
|
16
|
+
* `⌥` no Mac e `Alt` no resto: é a mesma tecla física, mas ninguém procura
|
|
17
|
+
* "Alt" num teclado da Apple. Fora do navegador (SSR) responde `Alt`, que é o
|
|
18
|
+
* que o primeiro render precisa escrever — a correção vem no cliente.
|
|
19
|
+
*/
|
|
20
|
+
export const tabShortcutLabel = (index: number): string => {
|
|
21
|
+
const mac =
|
|
22
|
+
typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform);
|
|
23
|
+
|
|
24
|
+
return `${mac ? "⌥" : "Alt"} ${index + 1}`;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Um atalho, uma instância.
|
|
29
|
+
*
|
|
30
|
+
* O `useShortcut` aceita uma lista de teclas mas chama o handler sem dizer
|
|
31
|
+
* **qual** disparou. Com um componente por aba, cada instância fica com a sua
|
|
32
|
+
* tecla e a sua ação, e nenhum hook roda dentro de laço — que é o que a regra
|
|
33
|
+
* dos hooks proíbe.
|
|
34
|
+
*/
|
|
35
|
+
const TabShortcut = ({
|
|
36
|
+
digit,
|
|
37
|
+
value,
|
|
38
|
+
onSelect,
|
|
39
|
+
}: {
|
|
40
|
+
digit: number;
|
|
41
|
+
value: string;
|
|
42
|
+
onSelect: (value: string) => void;
|
|
43
|
+
}): null => {
|
|
44
|
+
/*
|
|
45
|
+
Por `code`, e não por `key`: no macOS o `Option` + número não produz o
|
|
46
|
+
número — `⌥1` chega como `¡` e `⌥2` como `™`. Declarado como "1", o atalho
|
|
47
|
+
funcionava no Windows e falhava no Mac, que é o pior desfecho possível.
|
|
48
|
+
*/
|
|
49
|
+
useShortcut(`Digit${digit}`, () => onSelect(value), { alt: true, byCode: true });
|
|
50
|
+
|
|
51
|
+
return null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* `Alt` + `1..9` troca de aba. Não desenha nada.
|
|
56
|
+
*
|
|
57
|
+
* Com `Alt`, e não a tecla sozinha: os números precisam continuar sendo
|
|
58
|
+
* digitáveis, e o `1` sem modificador já é o atalho de abrir cadastro — dois
|
|
59
|
+
* donos para a mesma tecla é o conflito que só aparece na tela onde os dois
|
|
60
|
+
* existem.
|
|
61
|
+
*
|
|
62
|
+
* A ordem das abas é a fonte da numeração: passe a mesma lista que desenha os
|
|
63
|
+
* gatilhos, e não uma cópia. Duas fontes para a mesma ordem é o jeito garantido
|
|
64
|
+
* de `Alt + 3` abrir a aba errada no dia em que alguém reordenar a barra.
|
|
65
|
+
*/
|
|
66
|
+
export const TabShortcuts = ({ values, onSelect }: TabShortcutsProps): JSX.Element => (
|
|
67
|
+
<>
|
|
68
|
+
{values.slice(0, MAX_TAB_SHORTCUTS).map((value, index) => (
|
|
69
|
+
<TabShortcut
|
|
70
|
+
key={value}
|
|
71
|
+
digit={index + 1}
|
|
72
|
+
value={value}
|
|
73
|
+
onSelect={onSelect}
|
|
74
|
+
/>
|
|
75
|
+
))}
|
|
76
|
+
</>
|
|
77
|
+
);
|
|
@@ -49,7 +49,7 @@ export function useLoginFlow(): UseLoginFlowResult {
|
|
|
49
49
|
const router = useRouter();
|
|
50
50
|
const { notify } = useToast();
|
|
51
51
|
const { t } = useI18n();
|
|
52
|
-
const { run, loading, error, setError } = useRequest();
|
|
52
|
+
const { run, loading, error, setError } = useRequest({ silent: true });
|
|
53
53
|
const { refreshProfile } = useAuth();
|
|
54
54
|
|
|
55
55
|
const [step, setStep] = useState<LoginStep>("credentials");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { AlertTriangle, CheckCircle2, Info, XCircle } from "lucide-react";
|
|
3
|
+
import { AlertTriangle, CheckCircle2, Info, X, XCircle } from "lucide-react";
|
|
4
4
|
import type { JSX } from "react";
|
|
5
5
|
|
|
6
6
|
import { formatDateTime } from "#core/_utils/format";
|
|
@@ -23,26 +23,37 @@ const ICONS: Record<NotificationType, JSX.Element> = {
|
|
|
23
23
|
export interface NotificationItemProps {
|
|
24
24
|
notification: UserNotification;
|
|
25
25
|
onSelect: (notification: UserNotification) => void;
|
|
26
|
+
/** Tira esta notificação da caixa. */
|
|
27
|
+
onDismiss: (id: string) => void;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export function NotificationItem({
|
|
29
31
|
notification,
|
|
30
32
|
onSelect,
|
|
33
|
+
onDismiss,
|
|
31
34
|
}: NotificationItemProps): JSX.Element {
|
|
32
|
-
const { locale } = useI18n();
|
|
35
|
+
const { locale, t } = useI18n();
|
|
33
36
|
|
|
34
37
|
return (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
/*
|
|
39
|
+
O `×` é irmão do botão, e não filho: botão dentro de botão é HTML
|
|
40
|
+
inválido, e o clique de um dispararia o outro — descartar abriria a rota
|
|
41
|
+
da notificação no caminho.
|
|
42
|
+
*/
|
|
43
|
+
<div
|
|
38
44
|
className={cn(
|
|
39
|
-
"
|
|
45
|
+
"group/notice relative border-b border-border transition-colors hover:bg-accent",
|
|
40
46
|
!notification.read && "bg-accent/40",
|
|
41
47
|
)}
|
|
42
48
|
>
|
|
43
|
-
<
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
onClick={() => onSelect(notification)}
|
|
52
|
+
className="flex w-full gap-3 px-4 py-3 pr-10 text-left"
|
|
53
|
+
>
|
|
54
|
+
<span className="mt-0.5 shrink-0">{ICONS[notification.type]}</span>
|
|
44
55
|
|
|
45
|
-
|
|
56
|
+
<span className="min-w-0 flex-1">
|
|
46
57
|
<span className="flex items-center gap-2">
|
|
47
58
|
<span
|
|
48
59
|
className={cn(
|
|
@@ -63,7 +74,20 @@ export function NotificationItem({
|
|
|
63
74
|
<span className="mt-1 block text-[11px] text-muted-foreground">
|
|
64
75
|
{formatDateTime(notification.createdAt, locale)}
|
|
65
76
|
</span>
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
</span>
|
|
78
|
+
</button>
|
|
79
|
+
|
|
80
|
+
{/* Só no hover: um `×` fixo em cada linha vira ruído numa lista de vinte,
|
|
81
|
+
e o gesto de descartar não é o que se procura ao abrir o sino. */}
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
onClick={() => onDismiss(notification.id)}
|
|
85
|
+
aria-label={t("notifications.dismiss")}
|
|
86
|
+
title={t("notifications.dismiss")}
|
|
87
|
+
className="absolute right-2 top-3 hidden rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground group-hover/notice:block"
|
|
88
|
+
>
|
|
89
|
+
<X className="h-3.5 w-3.5" />
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
68
92
|
);
|
|
69
93
|
}
|
|
@@ -20,6 +20,10 @@ export interface UseNotificationsResult {
|
|
|
20
20
|
fetch: () => Promise<void>;
|
|
21
21
|
markAsRead: (id: string) => Promise<void>;
|
|
22
22
|
markAllAsRead: () => Promise<void>;
|
|
23
|
+
/** Tira uma da caixa deste usuário. */
|
|
24
|
+
dismiss: (id: string) => Promise<void>;
|
|
25
|
+
/** Limpa as já lidas — o "não deixa acumular". */
|
|
26
|
+
dismissRead: () => Promise<void>;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export function useNotifications(): UseNotificationsResult {
|
|
@@ -74,5 +78,32 @@ export function useNotifications(): UseNotificationsResult {
|
|
|
74
78
|
await run(() => notificationsService.markAllAsRead());
|
|
75
79
|
}, [run]);
|
|
76
80
|
|
|
77
|
-
|
|
81
|
+
/*
|
|
82
|
+
A lista recarrega em vez de tirar o item na mão: a paginação é do servidor,
|
|
83
|
+
e remover local deixaria a página com um registro a menos até o próximo
|
|
84
|
+
fetch — some uma notificação da contagem sem que ela tenha sido descartada.
|
|
85
|
+
*/
|
|
86
|
+
const dismiss = useCallback(
|
|
87
|
+
async (id: string): Promise<void> => {
|
|
88
|
+
await notificationsService.dismiss(id);
|
|
89
|
+
await fetch();
|
|
90
|
+
},
|
|
91
|
+
[fetch],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const dismissRead = useCallback(async (): Promise<void> => {
|
|
95
|
+
await notificationsService.dismissRead();
|
|
96
|
+
await fetch();
|
|
97
|
+
}, [fetch]);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
items,
|
|
101
|
+
unread,
|
|
102
|
+
loading,
|
|
103
|
+
fetch,
|
|
104
|
+
markAsRead,
|
|
105
|
+
markAllAsRead,
|
|
106
|
+
dismiss,
|
|
107
|
+
dismissRead,
|
|
108
|
+
};
|
|
78
109
|
}
|
|
@@ -18,7 +18,7 @@ import { useNotifications } from "#core/features/notifications/hooks/use-notific
|
|
|
18
18
|
import type { UserNotification } from "#core/features/notifications/services/notifications.service";
|
|
19
19
|
import { cn } from "#core/lib/utils";
|
|
20
20
|
|
|
21
|
-
type Tab = "all" | "unread";
|
|
21
|
+
type Tab = "all" | "unread" | "read";
|
|
22
22
|
|
|
23
23
|
/** Acima disso o número não cabe no badge sem empurrar o layout. */
|
|
24
24
|
const BADGE_LIMIT = 99;
|
|
@@ -26,11 +26,18 @@ const BADGE_LIMIT = 99;
|
|
|
26
26
|
export function NotificationsCenter(): JSX.Element {
|
|
27
27
|
const { t } = useI18n();
|
|
28
28
|
const router = useRouter();
|
|
29
|
-
const { items, unread, loading, markAsRead, markAllAsRead } =
|
|
29
|
+
const { items, unread, loading, markAsRead, markAllAsRead, dismiss, dismissRead } =
|
|
30
30
|
useNotifications();
|
|
31
31
|
const [tab, setTab] = useState<Tab>("all");
|
|
32
32
|
|
|
33
|
-
const visible =
|
|
33
|
+
const visible =
|
|
34
|
+
tab === "unread"
|
|
35
|
+
? items.filter((i) => !i.read)
|
|
36
|
+
: tab === "read"
|
|
37
|
+
? items.filter((i) => i.read)
|
|
38
|
+
: items;
|
|
39
|
+
|
|
40
|
+
const lidas = items.filter((i) => i.read).length;
|
|
34
41
|
|
|
35
42
|
const onSelect = (notification: UserNotification): void => {
|
|
36
43
|
if (!notification.read) {
|
|
@@ -63,7 +70,7 @@ export function NotificationsCenter(): JSX.Element {
|
|
|
63
70
|
<DropdownMenuContent align="end" className="w-96 p-0">
|
|
64
71
|
<div className="flex items-center justify-between border-b border-border px-4 py-2">
|
|
65
72
|
<div className="flex gap-3">
|
|
66
|
-
{(["all", "unread"] as const).map((value) => (
|
|
73
|
+
{(["all", "unread", "read"] as const).map((value) => (
|
|
67
74
|
<button
|
|
68
75
|
key={value}
|
|
69
76
|
type="button"
|
|
@@ -75,21 +82,35 @@ export function NotificationsCenter(): JSX.Element {
|
|
|
75
82
|
: "border-transparent text-muted-foreground hover:text-foreground",
|
|
76
83
|
)}
|
|
77
84
|
>
|
|
78
|
-
{value
|
|
79
|
-
? t("notifications.all")
|
|
80
|
-
: t("notifications.unread")}
|
|
85
|
+
{t(`notifications.${value}`)}
|
|
81
86
|
</button>
|
|
82
87
|
))}
|
|
83
88
|
</div>
|
|
84
89
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
{/*
|
|
91
|
+
A ação do cabeçalho segue a aba. Em "Lidas" o que se quer é
|
|
92
|
+
esvaziar, e "marcar todas como lidas" ali não faria nada — botão que
|
|
93
|
+
não pode fazer efeito é botão que ensina a pessoa a desconfiar.
|
|
94
|
+
*/}
|
|
95
|
+
{tab === "read" ? (
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
onClick={() => void dismissRead()}
|
|
99
|
+
disabled={lidas === 0}
|
|
100
|
+
className="text-xs font-medium text-primary hover:underline disabled:cursor-not-allowed disabled:text-muted-foreground disabled:no-underline"
|
|
101
|
+
>
|
|
102
|
+
{t("notifications.clearRead")}
|
|
103
|
+
</button>
|
|
104
|
+
) : (
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
onClick={() => void markAllAsRead()}
|
|
108
|
+
disabled={unread === 0}
|
|
109
|
+
className="text-xs font-medium text-primary hover:underline disabled:cursor-not-allowed disabled:text-muted-foreground disabled:no-underline"
|
|
110
|
+
>
|
|
111
|
+
{t("notifications.markAllRead")}
|
|
112
|
+
</button>
|
|
113
|
+
)}
|
|
93
114
|
</div>
|
|
94
115
|
|
|
95
116
|
<div className="max-h-96 overflow-y-auto">
|
|
@@ -110,6 +131,7 @@ export function NotificationsCenter(): JSX.Element {
|
|
|
110
131
|
key={notification.id}
|
|
111
132
|
notification={notification}
|
|
112
133
|
onSelect={onSelect}
|
|
134
|
+
onDismiss={(id) => void dismiss(id)}
|
|
113
135
|
/>
|
|
114
136
|
))}
|
|
115
137
|
</div>
|
|
@@ -35,4 +35,14 @@ export const notificationsService = {
|
|
|
35
35
|
markAllAsRead() {
|
|
36
36
|
return api.patch("/notifications/read-all", {}).then((r) => r.data);
|
|
37
37
|
},
|
|
38
|
+
|
|
39
|
+
/** Tira uma da caixa de quem chamou — não apaga para os outros. */
|
|
40
|
+
dismiss(id: string) {
|
|
41
|
+
return api.delete(`/notifications/${id}`).then(() => undefined);
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
/** Limpa as já lidas. Só elas: o não lido é o que não pode se perder. */
|
|
45
|
+
dismissRead() {
|
|
46
|
+
return api.delete("/notifications/read").then(() => undefined);
|
|
47
|
+
},
|
|
38
48
|
};
|
|
@@ -22,7 +22,7 @@ export function ForcePasswordChange({
|
|
|
22
22
|
}: {
|
|
23
23
|
reason?: "expired" | "provisional";
|
|
24
24
|
}): JSX.Element {
|
|
25
|
-
const { run, loading, error } = useRequest();
|
|
25
|
+
const { run, loading, error } = useRequest({ silent: true });
|
|
26
26
|
const { logout } = useAuth();
|
|
27
27
|
const { t } = useI18n();
|
|
28
28
|
|
|
@@ -53,10 +53,12 @@ export function ProfileScreen(): JSX.Element {
|
|
|
53
53
|
// `error` vinha sendo ignorado: o `useRequest` guarda a mensagem do servidor
|
|
54
54
|
// mas não notifica, então um 400 de validação ficava invisível — a tela não
|
|
55
55
|
// salvava e nada dizia por quê.
|
|
56
|
-
|
|
56
|
+
// Os dois calam o toast: esta tela desenha `error` acima do formulário, e
|
|
57
|
+
// a troca de senha desenha o dela dentro do bloco de senha.
|
|
58
|
+
const { run, loading, error, setError } = useRequest({ silent: true });
|
|
57
59
|
// useRequest próprio da troca de senha: o `run` compartilhado da tela zeraria
|
|
58
60
|
// esse erro a cada outra ação (salvar perfil, trocar avatar).
|
|
59
|
-
const password = useRequest();
|
|
61
|
+
const password = useRequest({ silent: true });
|
|
60
62
|
const { t } = useI18n();
|
|
61
63
|
|
|
62
64
|
const [avatar, setAvatar] = useState<string | null>(null);
|
|
@@ -35,7 +35,7 @@ export function ResetPasswordScreen(): JSX.Element {
|
|
|
35
35
|
const router = useRouter();
|
|
36
36
|
const params = useSearchParams();
|
|
37
37
|
const token = params.get("token") ?? "";
|
|
38
|
-
const { run, loading, error } = useRequest();
|
|
38
|
+
const { run, loading, error } = useRequest({ silent: true });
|
|
39
39
|
const { t } = useI18n();
|
|
40
40
|
const [done, setDone] = useState(false);
|
|
41
41
|
const [check, setCheck] = useState<PasswordTokenCheck | null>(null);
|
|
@@ -21,8 +21,6 @@ export interface Shortcut {
|
|
|
21
21
|
where: string;
|
|
22
22
|
/** Quando ele **não** dispara — a metade que evita surpresa. */
|
|
23
23
|
caveat: string;
|
|
24
|
-
/** Observação de teclado, quando a tecla tem outro nome em algum sistema. */
|
|
25
|
-
note?: string;
|
|
26
24
|
icon: React.ComponentType<{ className?: string }>;
|
|
27
25
|
}
|
|
28
26
|
|
|
@@ -88,9 +86,6 @@ export function ShortcutCard({ shortcut }: ShortcutCardProps): JSX.Element {
|
|
|
88
86
|
<Badge variant="secondary" className="w-fit font-normal">
|
|
89
87
|
{t(shortcut.caveat)}
|
|
90
88
|
</Badge>
|
|
91
|
-
{shortcut.note && (
|
|
92
|
-
<p className="text-xs text-muted-foreground">{t(shortcut.note)}</p>
|
|
93
|
-
)}
|
|
94
89
|
</div>
|
|
95
90
|
</Card>
|
|
96
91
|
);
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
Keyboard,
|
|
6
6
|
Maximize2,
|
|
7
7
|
PlusSquare,
|
|
8
|
+
Rows3,
|
|
8
9
|
XSquare,
|
|
9
10
|
} from "lucide-react";
|
|
10
11
|
import type { JSX } from "react";
|
|
@@ -15,8 +16,10 @@ import {
|
|
|
15
16
|
ShortcutCard,
|
|
16
17
|
} from "#core/features/shortcuts/components/shortcut-card";
|
|
17
18
|
import {
|
|
19
|
+
ALT_KEY_LABELS,
|
|
18
20
|
CREATE_SHORTCUT_KEYS,
|
|
19
|
-
|
|
21
|
+
EXPAND_SHORTCUT_KEY,
|
|
22
|
+
TAB_SHORTCUT_DIGITS,
|
|
20
23
|
} from "#core/hooks/use-shortcut";
|
|
21
24
|
|
|
22
25
|
/**
|
|
@@ -38,12 +41,20 @@ const SHORTCUTS: Shortcut[] = [
|
|
|
38
41
|
caveat: "shortcuts.create.caveat",
|
|
39
42
|
icon: PlusSquare,
|
|
40
43
|
},
|
|
44
|
+
{
|
|
45
|
+
name: "shortcuts.tabs.name",
|
|
46
|
+
// `Alt` e `⌥` são a mesma tecla: viram duas combinações e o card escreve
|
|
47
|
+
// "ou" entre elas, em vez de a diferença virar nota de rodapé.
|
|
48
|
+
combos: ALT_KEY_LABELS.map((alt) => [alt, TAB_SHORTCUT_DIGITS]),
|
|
49
|
+
where: "shortcuts.tabs.where",
|
|
50
|
+
caveat: "shortcuts.tabs.caveat",
|
|
51
|
+
icon: Rows3,
|
|
52
|
+
},
|
|
41
53
|
{
|
|
42
54
|
name: "shortcuts.expand.name",
|
|
43
|
-
combos:
|
|
55
|
+
combos: ALT_KEY_LABELS.map((alt) => [alt, EXPAND_SHORTCUT_KEY]),
|
|
44
56
|
where: "shortcuts.expand.where",
|
|
45
57
|
caveat: "shortcuts.expand.caveat",
|
|
46
|
-
note: "shortcuts.expand.note",
|
|
47
58
|
icon: Maximize2,
|
|
48
59
|
},
|
|
49
60
|
{
|
package/src/hooks/use-request.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { AxiosError } from "axios";
|
|
4
|
-
import { Dispatch, SetStateAction, useCallback, useState } from "react";
|
|
4
|
+
import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from "react";
|
|
5
5
|
|
|
6
6
|
import { useI18n, useToast } from "#core/contexts";
|
|
7
7
|
|
|
@@ -34,6 +34,18 @@ export interface RunOptions {
|
|
|
34
34
|
* "Registro criado" (ex.: "Link de primeiro acesso enviado").
|
|
35
35
|
*/
|
|
36
36
|
success?: RequestOperation | string;
|
|
37
|
+
/**
|
|
38
|
+
* Cala o toast de erro — para a tela que **já desenha** o `error` na página.
|
|
39
|
+
*
|
|
40
|
+
* O padrão é avisar, e é o certo: falha silenciosa é o pior desfecho
|
|
41
|
+
* possível de uma escrita. A pessoa clica em "Pagar", nada acontece, e não há
|
|
42
|
+
* como distinguir "o servidor recusou" de "o botão está quebrado" — as duas
|
|
43
|
+
* hipóteses levam a caminhos diferentes e nenhuma delas é investigar o log.
|
|
44
|
+
*
|
|
45
|
+
* O login é o caso legítimo do contrário: ele mostra "Credenciais inválidas"
|
|
46
|
+
* dentro do formulário, e o toast por cima diria a mesma coisa duas vezes.
|
|
47
|
+
*/
|
|
48
|
+
silent?: boolean;
|
|
37
49
|
/**
|
|
38
50
|
* Reage ao erro com o `errorCode` em mãos — para quando a tela precisa fazer
|
|
39
51
|
* algo além de mostrar a mensagem (ex.: voltar ao passo de credenciais
|
|
@@ -59,14 +71,33 @@ export interface UseRequestResult {
|
|
|
59
71
|
* Hook genérico para chamadas assíncronas com estados de loading/erro.
|
|
60
72
|
* Ex.: const { run, loading, error } = useRequest();
|
|
61
73
|
* await run(() => authService.login(email, senha));
|
|
74
|
+
*
|
|
75
|
+
* Os `defaults` valem para todas as chamadas daquele hook, e cada `run` os
|
|
76
|
+
* sobrescreve. É o que evita repetir `silent: true` em dez chamadas de uma tela
|
|
77
|
+
* que desenha o erro na própria página — e repetir dez vezes é como uma delas
|
|
78
|
+
* fica para trás.
|
|
62
79
|
*/
|
|
63
|
-
export function useRequest(): UseRequestResult {
|
|
80
|
+
export function useRequest(defaults: RunOptions = {}): UseRequestResult {
|
|
64
81
|
const { t } = useI18n();
|
|
65
82
|
const { notify } = useToast();
|
|
66
83
|
const [loading, setLoading] = useState(false);
|
|
84
|
+
/*
|
|
85
|
+
Os padrões entram por ref, e não pela lista de dependências do `run`.
|
|
86
|
+
|
|
87
|
+
A tela escreve `useRequest({ silent: true })`, e um literal é objeto novo a
|
|
88
|
+
cada render: pela dependência, o `run` mudaria de identidade sempre, e todo
|
|
89
|
+
`useCallback`/`useEffect` que depende dele — a carga de uma listagem, por
|
|
90
|
+
exemplo — recarregaria em laço. O sintoma seria a tela piscando sozinha, a
|
|
91
|
+
quilômetros da causa.
|
|
92
|
+
*/
|
|
93
|
+
const defaultsRef = useRef(defaults);
|
|
67
94
|
const [error, setError] = useState<string | null>(null);
|
|
68
95
|
const [errorCode, setErrorCode] = useState<string | null>(null);
|
|
69
96
|
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
defaultsRef.current = defaults;
|
|
99
|
+
}, [defaults]);
|
|
100
|
+
|
|
70
101
|
/**
|
|
71
102
|
* Ordem de preferência da mensagem:
|
|
72
103
|
* 1. tradução do `errorCode` — texto do produto, no idioma do usuário;
|
|
@@ -103,13 +134,14 @@ export function useRequest(): UseRequestResult {
|
|
|
103
134
|
);
|
|
104
135
|
|
|
105
136
|
const run = useCallback(
|
|
106
|
-
async <T>(fn: () => Promise<T>,
|
|
137
|
+
async <T>(fn: () => Promise<T>, call?: RunOptions): Promise<T | null> => {
|
|
138
|
+
const options = { ...defaultsRef.current, ...call };
|
|
107
139
|
setLoading(true);
|
|
108
140
|
setError(null);
|
|
109
141
|
setErrorCode(null);
|
|
110
142
|
try {
|
|
111
143
|
const result = await fn();
|
|
112
|
-
if (options
|
|
144
|
+
if (options.success) {
|
|
113
145
|
notify(successMessage(options.success), "success");
|
|
114
146
|
}
|
|
115
147
|
return result;
|
|
@@ -120,7 +152,17 @@ export function useRequest(): UseRequestResult {
|
|
|
120
152
|
const code = body?.errorCode ?? null;
|
|
121
153
|
setError(message);
|
|
122
154
|
setErrorCode(code);
|
|
123
|
-
|
|
155
|
+
|
|
156
|
+
/*
|
|
157
|
+
O aviso vem antes do `onError` de propósito: quem passa o callback
|
|
158
|
+
costuma navegar ou fechar um modal ali dentro, e o toast disparado
|
|
159
|
+
depois disso apareceria sobre uma tela que já mudou.
|
|
160
|
+
*/
|
|
161
|
+
if (!options.silent) {
|
|
162
|
+
notify(message, "error");
|
|
163
|
+
}
|
|
164
|
+
options.onError?.({ code, message });
|
|
165
|
+
|
|
124
166
|
return null;
|
|
125
167
|
} finally {
|
|
126
168
|
setLoading(false);
|
|
@@ -47,6 +47,26 @@ export const EXPAND_SHORTCUT_KEY = "Enter";
|
|
|
47
47
|
/** Como ele se apresenta ao usuário. */
|
|
48
48
|
export const EXPAND_SHORTCUT_LABEL = "Alt + Enter";
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* O modificador do `Alt`, escrito das duas formas que os teclados usam.
|
|
52
|
+
*
|
|
53
|
+
* `Alt` e `⌥` são a **mesma tecla física**, e é por isso que aparecem como
|
|
54
|
+
* combinações alternativas na tela de atalhos em vez de virarem uma nota de
|
|
55
|
+
* rodapé: quem tem um teclado da Apple procura o símbolo, não a palavra.
|
|
56
|
+
*/
|
|
57
|
+
export const ALT_KEY_LABELS = ["Alt", "⌥"] as const;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Quantas abas o atalho numera.
|
|
61
|
+
*
|
|
62
|
+
* Nove porque `Alt + 0` não segue a sequência, e porque ninguém conta abas além
|
|
63
|
+
* disso sem olhar.
|
|
64
|
+
*/
|
|
65
|
+
export const MAX_TAB_SHORTCUTS = 9;
|
|
66
|
+
|
|
67
|
+
/** Como o atalho de abas se apresenta: `Alt` + `1…9`. */
|
|
68
|
+
export const TAB_SHORTCUT_DIGITS = `1…${MAX_TAB_SHORTCUTS}`;
|
|
69
|
+
|
|
50
70
|
export interface ShortcutOptions {
|
|
51
71
|
/**
|
|
52
72
|
* Exige duas batidas seguidas dentro de meio segundo.
|
|
@@ -85,6 +105,28 @@ export interface ShortcutOptions {
|
|
|
85
105
|
* `false` quando a tecla sem ele significar outra coisa.
|
|
86
106
|
*/
|
|
87
107
|
shift?: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Compara a **posição física** da tecla (`event.code`), e não o caractere.
|
|
110
|
+
*
|
|
111
|
+
* Existe por causa do `Alt`: no macOS, `Option` + número **não** produz o
|
|
112
|
+
* número — produz um símbolo. `⌥1` chega como `¡`, `⌥2` como `™`, `⌥3` como
|
|
113
|
+
* `£`, e o atalho declarado como `"1"` nunca dispara. No Windows e no Linux
|
|
114
|
+
* ele dispararia, o que é pior que não funcionar em lugar nenhum: quebra só
|
|
115
|
+
* na máquina de quem usa Mac.
|
|
116
|
+
*
|
|
117
|
+
* `event.code` é imune a isso e a layout: a tecla ao lado do `Q` é `KeyW` no
|
|
118
|
+
* QWERTY e no AZERTY, e o `1` é `Digit1` com ou sem modificador.
|
|
119
|
+
*
|
|
120
|
+
* Em troca, ele **ignora o layout de propósito** — e é justamente por isso
|
|
121
|
+
* que não é o padrão. Um atalho de letra declarado por `code` cai na tecla
|
|
122
|
+
* errada de quem usa Dvorak, enquanto por `key` ele segue a letra que a
|
|
123
|
+
* pessoa vê impressa. Use `byCode` para dígito com modificador; para letra,
|
|
124
|
+
* continue em `key`.
|
|
125
|
+
*
|
|
126
|
+
* Os valores são os do padrão: `Digit1`..`Digit9`, `KeyA`..`KeyZ`,
|
|
127
|
+
* `Numpad1`, `Enter`, `Space`.
|
|
128
|
+
*/
|
|
129
|
+
byCode?: boolean;
|
|
88
130
|
/**
|
|
89
131
|
* Deixa o atalho valer também com o cursor dentro de um campo.
|
|
90
132
|
*
|
|
@@ -146,6 +188,7 @@ export function useShortcut(
|
|
|
146
188
|
shift,
|
|
147
189
|
allowInDialog = false,
|
|
148
190
|
allowWhileTyping = false,
|
|
191
|
+
byCode = false,
|
|
149
192
|
} = options;
|
|
150
193
|
|
|
151
194
|
// Serializado para o efeito não se reinscrever a cada render quando a tela
|
|
@@ -179,7 +222,12 @@ export function useShortcut(
|
|
|
179
222
|
const accepted = keySignature.split("\u0000");
|
|
180
223
|
|
|
181
224
|
const onKeyDown = (event: KeyboardEvent): void => {
|
|
182
|
-
|
|
225
|
+
// A mesma leitura serve para a comparação e para a janela do duplo: são
|
|
226
|
+
// "a mesma tecla", e ler uma por `code` e outra por `key` faria a segunda
|
|
227
|
+
// batida de um atalho por posição nunca casar com a primeira.
|
|
228
|
+
const pressed = byCode ? event.code : event.key;
|
|
229
|
+
|
|
230
|
+
if (!accepted.includes(pressed)) {
|
|
183
231
|
return;
|
|
184
232
|
}
|
|
185
233
|
if (event.altKey !== alt) {
|
|
@@ -203,10 +251,10 @@ export function useShortcut(
|
|
|
203
251
|
if (double) {
|
|
204
252
|
const now = Date.now();
|
|
205
253
|
const previous = lastPressRef.current;
|
|
206
|
-
lastPressRef.current = { key:
|
|
254
|
+
lastPressRef.current = { key: pressed, at: now };
|
|
207
255
|
|
|
208
256
|
if (
|
|
209
|
-
previous.key !==
|
|
257
|
+
previous.key !== pressed ||
|
|
210
258
|
now - previous.at > DOUBLE_PRESS_WINDOW_MS
|
|
211
259
|
) {
|
|
212
260
|
return;
|
|
@@ -244,6 +292,7 @@ export function useShortcut(
|
|
|
244
292
|
shift,
|
|
245
293
|
allowInDialog,
|
|
246
294
|
allowWhileTyping,
|
|
295
|
+
byCode,
|
|
247
296
|
]);
|
|
248
297
|
}
|
|
249
298
|
|
package/src/i18n/messages/en.ts
CHANGED
|
@@ -142,12 +142,17 @@ export const en: Messages = {
|
|
|
142
142
|
where: "On any listing with a create button.",
|
|
143
143
|
caveat: "Does not fire inside a text field or while a dialog is open",
|
|
144
144
|
},
|
|
145
|
+
tabs: {
|
|
146
|
+
name: "Switch tab",
|
|
147
|
+
where:
|
|
148
|
+
"On any screen with tabs, the number opens the tab in that position — 1 is the first.",
|
|
149
|
+
caveat: "Does not fire inside a text field or with a modal open",
|
|
150
|
+
},
|
|
145
151
|
expand: {
|
|
146
152
|
name: "Expand the dialog",
|
|
147
153
|
where:
|
|
148
154
|
"With a dialog open, toggles between normal and expanded size. Works anywhere in the dialog, like Esc.",
|
|
149
155
|
caveat: "Works in any field, and does not submit the form",
|
|
150
|
-
note: "On a Mac, Alt is the Option key (⌥) — most keyboards print both labels on it.",
|
|
151
156
|
},
|
|
152
157
|
submit: {
|
|
153
158
|
name: "Submit the form",
|
|
@@ -170,6 +175,8 @@ export const en: Messages = {
|
|
|
170
175
|
},
|
|
171
176
|
table: {
|
|
172
177
|
columns: "Columns",
|
|
178
|
+
selectAll: "Select all rows on this page",
|
|
179
|
+
selectRow: "Select this row",
|
|
173
180
|
columnsButton: "Columns",
|
|
174
181
|
columnsTitle: "Configure column visibility",
|
|
175
182
|
columnsHint: "Select the columns you want to see in the table.",
|
|
@@ -252,6 +259,9 @@ export const en: Messages = {
|
|
|
252
259
|
title: "Notifications",
|
|
253
260
|
all: "All",
|
|
254
261
|
unread: "Unread",
|
|
262
|
+
read: "Read",
|
|
263
|
+
clearRead: "Clear read",
|
|
264
|
+
dismiss: "Remove from list",
|
|
255
265
|
markAllRead: "Mark all as read",
|
|
256
266
|
empty: "No notifications",
|
|
257
267
|
},
|
package/src/i18n/messages/pt.ts
CHANGED
|
@@ -144,12 +144,17 @@ export const pt = {
|
|
|
144
144
|
where: "Em qualquer listagem que tenha o botão de cadastrar.",
|
|
145
145
|
caveat: "Não dispara dentro de um campo de texto nem com um modal aberto",
|
|
146
146
|
},
|
|
147
|
+
tabs: {
|
|
148
|
+
name: "Trocar de aba",
|
|
149
|
+
where:
|
|
150
|
+
"Em qualquer tela com abas, o número abre a aba naquela posição — 1 é a primeira.",
|
|
151
|
+
caveat: "Não dispara dentro de um campo de texto nem com um modal aberto",
|
|
152
|
+
},
|
|
147
153
|
expand: {
|
|
148
154
|
name: "Ampliar o modal",
|
|
149
155
|
where:
|
|
150
156
|
"Com um modal aberto, alterna entre o tamanho normal e o ampliado. Vale em qualquer lugar do modal, como o Esc.",
|
|
151
157
|
caveat: "Funciona em qualquer campo, e não envia o formulário junto",
|
|
152
|
-
note: "No Mac, o Alt é a tecla Option (⌥) — em boa parte dos teclados ela vem escrita das duas formas.",
|
|
153
158
|
},
|
|
154
159
|
submit: {
|
|
155
160
|
name: "Enviar o formulário",
|
|
@@ -172,6 +177,8 @@ export const pt = {
|
|
|
172
177
|
},
|
|
173
178
|
table: {
|
|
174
179
|
columns: "Colunas",
|
|
180
|
+
selectAll: "Selecionar todas as linhas da página",
|
|
181
|
+
selectRow: "Selecionar esta linha",
|
|
175
182
|
columnsButton: "Colunas",
|
|
176
183
|
columnsTitle: "Configurar visualização de colunas",
|
|
177
184
|
columnsHint: "Selecione as colunas que deseja visualizar na tabela.",
|
|
@@ -255,6 +262,9 @@ export const pt = {
|
|
|
255
262
|
title: "Notificações",
|
|
256
263
|
all: "Todas",
|
|
257
264
|
unread: "Não lidas",
|
|
265
|
+
read: "Lidas",
|
|
266
|
+
clearRead: "Limpar lidas",
|
|
267
|
+
dismiss: "Tirar da lista",
|
|
258
268
|
markAllRead: "Marcar todas como lidas",
|
|
259
269
|
empty: "Nenhuma notificação",
|
|
260
270
|
},
|
package/src/index.ts
CHANGED
|
@@ -106,10 +106,13 @@ export { useListQuery } from "#core/hooks/use-list-query";
|
|
|
106
106
|
export type { RunOptions, UseRequestResult } from "#core/hooks/use-request";
|
|
107
107
|
export type { ShortcutOptions } from "#core/hooks/use-shortcut";
|
|
108
108
|
export {
|
|
109
|
+
ALT_KEY_LABELS,
|
|
109
110
|
CREATE_SHORTCUT_KEYS,
|
|
110
111
|
CREATE_SHORTCUT_LABEL,
|
|
111
112
|
EXPAND_SHORTCUT_KEY,
|
|
112
113
|
EXPAND_SHORTCUT_LABEL,
|
|
114
|
+
MAX_TAB_SHORTCUTS,
|
|
115
|
+
TAB_SHORTCUT_DIGITS,
|
|
113
116
|
useShortcut,
|
|
114
117
|
} from "#core/hooks/use-shortcut";
|
|
115
118
|
export type { TableView, UseTableViewResult } from "#core/hooks/use-table-view";
|