rl-core-front 0.15.4 → 0.15.6
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/_utils/calendar.ts +211 -7
- package/src/components/ui/data-table.tsx +632 -247
- package/src/components/ui/date-picker.tsx +84 -10
- package/src/components/ui/dialog.tsx +81 -1
- package/src/components/ui/table.tsx +30 -1
- package/src/hooks/use-column-visibility.ts +6 -1
- package/src/hooks/use-table-view.ts +54 -0
- package/src/i18n/messages/en.ts +5 -0
- package/src/i18n/messages/pt.ts +5 -0
- package/src/index.ts +2 -0
|
@@ -6,12 +6,15 @@ import { useState } from "react";
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
addMonths,
|
|
9
|
+
dayPlaceholder,
|
|
9
10
|
formatDay,
|
|
10
11
|
fromIsoDay,
|
|
11
12
|
isSameDay,
|
|
12
13
|
isSameMonth,
|
|
14
|
+
maskDay,
|
|
13
15
|
monthGrid,
|
|
14
16
|
monthLabel,
|
|
17
|
+
parseTypedDay,
|
|
15
18
|
today,
|
|
16
19
|
toIsoDay,
|
|
17
20
|
weekdayLabels,
|
|
@@ -19,6 +22,7 @@ import {
|
|
|
19
22
|
import { Button } from "#core/components/ui/button";
|
|
20
23
|
import {
|
|
21
24
|
Popover,
|
|
25
|
+
PopoverAnchor,
|
|
22
26
|
PopoverContent,
|
|
23
27
|
PopoverTrigger,
|
|
24
28
|
} from "#core/components/ui/popover";
|
|
@@ -46,6 +50,10 @@ export interface DatePickerProps {
|
|
|
46
50
|
* O valor continua sendo a string ISO do dia, e não `Date`: é o que o backend
|
|
47
51
|
* grava (`char(10)`) e o que evita a viagem por fuso que transforma dia 1º às
|
|
48
52
|
* 00:00 locais no dia 30 em UTC.
|
|
53
|
+
*
|
|
54
|
+
* O campo é **digitável**: `1108` já é 11 de agosto, e quem sabe a data não
|
|
55
|
+
* precisa navegar meses no calendário para dizê-la. O calendário continua ali,
|
|
56
|
+
* no ícone — é o caminho de quem procura o dia, não de quem já o conhece.
|
|
49
57
|
*/
|
|
50
58
|
export function DatePicker({
|
|
51
59
|
value,
|
|
@@ -56,15 +64,55 @@ export function DatePicker({
|
|
|
56
64
|
}: DatePickerProps): JSX.Element {
|
|
57
65
|
const { t, locale } = useI18n();
|
|
58
66
|
const [open, setOpen] = useState(false);
|
|
67
|
+
/*
|
|
68
|
+
O que está sendo digitado, enquanto ainda não é uma data.
|
|
69
|
+
|
|
70
|
+
Sem este rascunho o campo teria de apagar cada tecla que não fecha um dia
|
|
71
|
+
válido — digitar `1` já voltaria para o valor gravado, e não haveria como
|
|
72
|
+
chegar ao `11`.
|
|
73
|
+
*/
|
|
74
|
+
const [typed, setTyped] = useState<string | null>(null);
|
|
59
75
|
|
|
60
76
|
const selected = fromIsoDay(value);
|
|
61
77
|
const [month, setMonth] = useState<Date>(() => selected ?? today());
|
|
62
78
|
|
|
63
79
|
const pickDay = (day: Date): void => {
|
|
64
80
|
onChange(toIsoDay(day));
|
|
81
|
+
setTyped(null);
|
|
65
82
|
setOpen(false);
|
|
66
83
|
};
|
|
67
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Cada tecla: a máscara põe as barras, e a data sai assim que existir.
|
|
87
|
+
*
|
|
88
|
+
* O ano em branco é o do dia que está no campo — ou o de hoje, quando não há
|
|
89
|
+
* nenhum. Campo esvaziado limpa o valor: apagar é uma escolha, não um
|
|
90
|
+
* rascunho pela metade.
|
|
91
|
+
*/
|
|
92
|
+
const digitar = (raw: string): void => {
|
|
93
|
+
// O texto que estava lá antes: é ele que diz se a tecla foi um dígito novo
|
|
94
|
+
// ou um apagar — e o campo se comporta diferente nos dois casos.
|
|
95
|
+
const texto = maskDay(raw, locale, typed ?? (value ? formatDay(value, locale) : ""));
|
|
96
|
+
setTyped(texto);
|
|
97
|
+
|
|
98
|
+
if (texto === "") {
|
|
99
|
+
onChange(undefined);
|
|
100
|
+
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const iso = parseTypedDay(
|
|
105
|
+
texto,
|
|
106
|
+
locale,
|
|
107
|
+
(selected ?? today()).getFullYear(),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (iso) {
|
|
111
|
+
onChange(iso);
|
|
112
|
+
setMonth(fromIsoDay(iso) ?? month);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
68
116
|
const dayClass = (day: Date): string => {
|
|
69
117
|
if (selected && isSameDay(day, selected)) {
|
|
70
118
|
return "bg-primary text-primary-foreground font-semibold";
|
|
@@ -77,16 +125,41 @@ export function DatePicker({
|
|
|
77
125
|
|
|
78
126
|
return (
|
|
79
127
|
<Popover open={open} onOpenChange={setOpen}>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
>
|
|
85
|
-
<
|
|
86
|
-
{
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
128
|
+
{/*
|
|
129
|
+
A âncora é a linha inteira, e não o ícone: presa ao botão, o painel
|
|
130
|
+
abriria alinhado a ele e com a largura dele — um calendário de 16px.
|
|
131
|
+
*/}
|
|
132
|
+
<PopoverAnchor asChild>
|
|
133
|
+
<div
|
|
134
|
+
className={cn(
|
|
135
|
+
"flex h-10 w-full items-center rounded-md border border-input bg-transparent shadow-sm transition-colors hover:border-ring focus-within:ring-2 focus-within:ring-ring",
|
|
136
|
+
disabled && "cursor-not-allowed opacity-50",
|
|
137
|
+
)}
|
|
138
|
+
>
|
|
139
|
+
<input
|
|
140
|
+
aria-label={label}
|
|
141
|
+
value={typed ?? (value ? formatDay(value, locale) : "")}
|
|
142
|
+
disabled={disabled}
|
|
143
|
+
// Só dígitos, e o teclado do celular abre no numérico: a máscara é
|
|
144
|
+
// quem escreve as barras.
|
|
145
|
+
inputMode="numeric"
|
|
146
|
+
placeholder={placeholder ?? dayPlaceholder(locale)}
|
|
147
|
+
onChange={(event) => digitar(event.target.value)}
|
|
148
|
+
// Sai do campo e o rascunho some: `11/0` não é data, e deixá-lo
|
|
149
|
+
// escrito faria o campo mostrar uma coisa e o formulário guardar
|
|
150
|
+
// outra.
|
|
151
|
+
onBlur={() => setTyped(null)}
|
|
152
|
+
className="h-full w-full min-w-0 bg-transparent px-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed"
|
|
153
|
+
/>
|
|
154
|
+
<PopoverTrigger
|
|
155
|
+
disabled={disabled}
|
|
156
|
+
aria-label={t("filters.openCalendar")}
|
|
157
|
+
className="flex h-full shrink-0 items-center px-3 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed"
|
|
158
|
+
>
|
|
159
|
+
<Calendar className="h-4 w-4" />
|
|
160
|
+
</PopoverTrigger>
|
|
161
|
+
</div>
|
|
162
|
+
</PopoverAnchor>
|
|
90
163
|
|
|
91
164
|
<PopoverContent
|
|
92
165
|
// Largura do campo, e não fixa: o painel é a continuação dele, e
|
|
@@ -160,6 +233,7 @@ export function DatePicker({
|
|
|
160
233
|
size="sm"
|
|
161
234
|
onClick={() => {
|
|
162
235
|
onChange(undefined);
|
|
236
|
+
setTyped(null);
|
|
163
237
|
setOpen(false);
|
|
164
238
|
}}
|
|
165
239
|
>
|
|
@@ -4,6 +4,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
|
4
4
|
import { Expand, Shrink, X } from "lucide-react";
|
|
5
5
|
import * as React from "react";
|
|
6
6
|
|
|
7
|
+
import { Button, type ButtonProps } from "#core/components/ui/button";
|
|
7
8
|
import {
|
|
8
9
|
EXPAND_SHORTCUT_KEY,
|
|
9
10
|
EXPAND_SHORTCUT_LABEL,
|
|
@@ -11,6 +12,25 @@ import {
|
|
|
11
12
|
} from "#core/hooks/use-shortcut";
|
|
12
13
|
import { cn } from "#core/lib/utils";
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Como pedir para fechar o modal **passando pela guarda**.
|
|
17
|
+
*
|
|
18
|
+
* O `Esc`, o clique fora e o `X` passam pelo `DialogContent`, que pergunta
|
|
19
|
+
* antes de descartar o que foi preenchido. O botão "Cancelar" mora na tela e
|
|
20
|
+
* chamava o `onClose` dela direto — mesma saída, sem a pergunta. Este contexto
|
|
21
|
+
* é o caminho que faltava: quem pede daqui recebe a mesma confirmação.
|
|
22
|
+
*/
|
|
23
|
+
const DialogCloseRequestContext = React.createContext<() => void>(() => {});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Pede o fechamento do modal em que o componente está.
|
|
27
|
+
*
|
|
28
|
+
* Fora de um `DialogContent` não faz nada — nunca quebra, só não tem o que
|
|
29
|
+
* fechar.
|
|
30
|
+
*/
|
|
31
|
+
const useDialogCloseRequest = (): (() => void) =>
|
|
32
|
+
React.useContext(DialogCloseRequestContext);
|
|
33
|
+
|
|
14
34
|
const Dialog = DialogPrimitive.Root;
|
|
15
35
|
const DialogTrigger = DialogPrimitive.Trigger;
|
|
16
36
|
const DialogPortal = DialogPrimitive.Portal;
|
|
@@ -248,6 +268,22 @@ const DialogContent = React.forwardRef<
|
|
|
248
268
|
setConfirmandoSaida(true);
|
|
249
269
|
};
|
|
250
270
|
|
|
271
|
+
/**
|
|
272
|
+
* A saída pedida de dentro do corpo — é o que o "Cancelar" usa.
|
|
273
|
+
*
|
|
274
|
+
* Sem evento para segurar: aqui o fechamento nem começa, ou é a pergunta que
|
|
275
|
+
* aparece, ou é o `Close` escondido que fecha de uma vez.
|
|
276
|
+
*/
|
|
277
|
+
const pedirSaida = React.useCallback((): void => {
|
|
278
|
+
if (unsavedChanges) {
|
|
279
|
+
setConfirmandoSaida(true);
|
|
280
|
+
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
fecharRef.current?.click();
|
|
285
|
+
}, [unsavedChanges]);
|
|
286
|
+
|
|
251
287
|
// O cabeçalho sai da lista para dividir a primeira linha com os botões; o
|
|
252
288
|
// resto segue na ordem em que o chamador escreveu.
|
|
253
289
|
const items = React.Children.toArray(children);
|
|
@@ -338,7 +374,9 @@ const DialogContent = React.forwardRef<
|
|
|
338
374
|
</DialogPrimitive.Close>
|
|
339
375
|
</div>
|
|
340
376
|
</div>
|
|
341
|
-
{
|
|
377
|
+
<DialogCloseRequestContext.Provider value={pedirSaida}>
|
|
378
|
+
{body}
|
|
379
|
+
</DialogCloseRequestContext.Provider>
|
|
342
380
|
|
|
343
381
|
<DialogPrimitive.Close ref={fecharRef} className="hidden" tabIndex={-1} />
|
|
344
382
|
</DialogPrimitive.Content>
|
|
@@ -467,8 +505,49 @@ const DialogDescription = React.forwardRef<
|
|
|
467
505
|
));
|
|
468
506
|
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
|
469
507
|
|
|
508
|
+
/**
|
|
509
|
+
* O "Cancelar" do rodapé — a mesma saída do `Esc`, com a mesma pergunta.
|
|
510
|
+
*
|
|
511
|
+
* Existe porque o botão que a tela escrevia à mão chamava o `onClose` dela
|
|
512
|
+
* direto: fechava sem perguntar nada, e o formulário preenchido ia junto.
|
|
513
|
+
* Aqui o fechamento é pedido ao modal, que decide se pergunta antes.
|
|
514
|
+
*
|
|
515
|
+
* O texto vem de fora — este arquivo não depende do provider de i18n, e passar
|
|
516
|
+
* a depender quebraria todo uso fora dele.
|
|
517
|
+
*
|
|
518
|
+
* ```tsx
|
|
519
|
+
* <DialogFooter>
|
|
520
|
+
* <DialogCancel>{t("common.cancel")}</DialogCancel>
|
|
521
|
+
* <Button type="submit">{t("common.save")}</Button>
|
|
522
|
+
* </DialogFooter>
|
|
523
|
+
* ```
|
|
524
|
+
*/
|
|
525
|
+
const DialogCancel = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
526
|
+
({ variant = "outline", type = "button", onClick, ...props }, ref) => {
|
|
527
|
+
const pedirSaida = useDialogCloseRequest();
|
|
528
|
+
|
|
529
|
+
return (
|
|
530
|
+
<Button
|
|
531
|
+
ref={ref}
|
|
532
|
+
type={type}
|
|
533
|
+
variant={variant}
|
|
534
|
+
onClick={(event) => {
|
|
535
|
+
onClick?.(event);
|
|
536
|
+
|
|
537
|
+
if (!event.defaultPrevented) {
|
|
538
|
+
pedirSaida();
|
|
539
|
+
}
|
|
540
|
+
}}
|
|
541
|
+
{...props}
|
|
542
|
+
/>
|
|
543
|
+
);
|
|
544
|
+
},
|
|
545
|
+
);
|
|
546
|
+
DialogCancel.displayName = "DialogCancel";
|
|
547
|
+
|
|
470
548
|
export {
|
|
471
549
|
Dialog,
|
|
550
|
+
DialogCancel,
|
|
472
551
|
DialogClose,
|
|
473
552
|
DialogContent,
|
|
474
553
|
DialogDescription,
|
|
@@ -479,4 +558,5 @@ export {
|
|
|
479
558
|
DialogPortal,
|
|
480
559
|
DialogTitle,
|
|
481
560
|
DialogTrigger,
|
|
561
|
+
useDialogCloseRequest,
|
|
482
562
|
};
|
|
@@ -36,6 +36,27 @@ const TableBody = React.forwardRef<
|
|
|
36
36
|
));
|
|
37
37
|
TableBody.displayName = "TableBody";
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* A linha de fechamento da tabela: total, saldo, contagem.
|
|
41
|
+
*
|
|
42
|
+
* `tfoot` e não uma última linha do corpo: ela não é registro — não ordena
|
|
43
|
+
* junto, não entra na contagem e continua embaixo quando a ordenação muda.
|
|
44
|
+
*/
|
|
45
|
+
const TableFooter = React.forwardRef<
|
|
46
|
+
HTMLTableSectionElement,
|
|
47
|
+
React.HTMLAttributes<HTMLTableSectionElement>
|
|
48
|
+
>(({ className, ...props }, ref) => (
|
|
49
|
+
<tfoot
|
|
50
|
+
ref={ref}
|
|
51
|
+
className={cn(
|
|
52
|
+
"border-t border-border bg-muted/40 font-medium [&_tr]:border-0",
|
|
53
|
+
className,
|
|
54
|
+
)}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
));
|
|
58
|
+
TableFooter.displayName = "TableFooter";
|
|
59
|
+
|
|
39
60
|
const TableRow = React.forwardRef<
|
|
40
61
|
HTMLTableRowElement,
|
|
41
62
|
React.HTMLAttributes<HTMLTableRowElement>
|
|
@@ -90,4 +111,12 @@ const TableCell = React.forwardRef<
|
|
|
90
111
|
));
|
|
91
112
|
TableCell.displayName = "TableCell";
|
|
92
113
|
|
|
93
|
-
export {
|
|
114
|
+
export {
|
|
115
|
+
Table,
|
|
116
|
+
TableBody,
|
|
117
|
+
TableCell,
|
|
118
|
+
TableFooter,
|
|
119
|
+
TableHead,
|
|
120
|
+
TableHeader,
|
|
121
|
+
TableRow,
|
|
122
|
+
};
|
|
@@ -15,11 +15,16 @@ export interface UseColumnVisibilityResult {
|
|
|
15
15
|
/**
|
|
16
16
|
* Persiste as colunas ocultas de uma tabela em localStorage (por storageKey).
|
|
17
17
|
* Retorna a lista de ocultas + helpers para ler/salvar.
|
|
18
|
+
*
|
|
19
|
+
* `defaultHidden` é só o ponto de partida: vale enquanto ninguém tiver
|
|
20
|
+
* escolhido as colunas nesta tela. Depois da primeira escolha, a lista guardada
|
|
21
|
+
* é a verdade — inclusive quando ela mostra uma coluna que nasceu escondida.
|
|
18
22
|
*/
|
|
19
23
|
export function useColumnVisibility(
|
|
20
24
|
storageKey?: string,
|
|
25
|
+
defaultHidden: string[] = [],
|
|
21
26
|
): UseColumnVisibilityResult {
|
|
22
|
-
const [hiddenColumns, setHiddenColumns] = useState<string[]>(
|
|
27
|
+
const [hiddenColumns, setHiddenColumns] = useState<string[]>(defaultHidden);
|
|
23
28
|
|
|
24
29
|
useEffect(() => {
|
|
25
30
|
if (!storageKey) {return;}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import { safeStorage } from "#core/_utils/storage";
|
|
6
|
+
|
|
7
|
+
const STORAGE_PREFIX = "view:";
|
|
8
|
+
|
|
9
|
+
/** Como a listagem se apresenta: em linhas ou em cartões. */
|
|
10
|
+
export type TableView = "list" | "grid";
|
|
11
|
+
|
|
12
|
+
export interface UseTableViewResult {
|
|
13
|
+
view: TableView;
|
|
14
|
+
setView: (next: TableView) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Guarda em qual formato a pessoa deixou a listagem, por `storageKey`.
|
|
19
|
+
*
|
|
20
|
+
* Mesma ideia das colunas escondidas: é escolha de quem olha, não da tela, e
|
|
21
|
+
* refazê-la a cada visita transformaria o alternador num incômodo. Sem
|
|
22
|
+
* `storageKey` o formato dura só enquanto a tela estiver aberta — igual às
|
|
23
|
+
* colunas, que também só persistem quando a tabela tem chave.
|
|
24
|
+
*/
|
|
25
|
+
export function useTableView(
|
|
26
|
+
storageKey?: string,
|
|
27
|
+
defaultView: TableView = "list",
|
|
28
|
+
): UseTableViewResult {
|
|
29
|
+
const [view, setStoredView] = useState<TableView>(defaultView);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!storageKey) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const raw = safeStorage.get(`${STORAGE_PREFIX}${storageKey}`);
|
|
36
|
+
|
|
37
|
+
if (raw === "list" || raw === "grid") {
|
|
38
|
+
setStoredView(raw);
|
|
39
|
+
}
|
|
40
|
+
}, [storageKey]);
|
|
41
|
+
|
|
42
|
+
const setView = useCallback(
|
|
43
|
+
(next: TableView) => {
|
|
44
|
+
setStoredView(next);
|
|
45
|
+
|
|
46
|
+
if (storageKey) {
|
|
47
|
+
safeStorage.set(`${STORAGE_PREFIX}${storageKey}`, next);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
[storageKey],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return { view, setView };
|
|
54
|
+
}
|
package/src/i18n/messages/en.ts
CHANGED
|
@@ -165,8 +165,12 @@ export const en: Messages = {
|
|
|
165
165
|
nextPage: "Next page",
|
|
166
166
|
empty: "No records",
|
|
167
167
|
emptyFiltered: "No records match this filter",
|
|
168
|
+
view: "List format",
|
|
169
|
+
viewList: "List",
|
|
170
|
+
viewGrid: "Cards",
|
|
168
171
|
reorder: "Drag to reorder",
|
|
169
172
|
reorderSorted: "Clear the column sorting to drag rows",
|
|
173
|
+
reorderGrouped: "Rows cannot be reordered while the list is grouped",
|
|
170
174
|
},
|
|
171
175
|
filters: {
|
|
172
176
|
button: "Filters",
|
|
@@ -183,6 +187,7 @@ export const en: Messages = {
|
|
|
183
187
|
toChip: "until",
|
|
184
188
|
selectPeriod: "Select a period",
|
|
185
189
|
selectDate: "Select a date",
|
|
190
|
+
openCalendar: "Open calendar",
|
|
186
191
|
today: "Today",
|
|
187
192
|
lastDays: "{days} days",
|
|
188
193
|
previousMonth: "Previous month",
|
package/src/i18n/messages/pt.ts
CHANGED
|
@@ -167,9 +167,13 @@ export const pt = {
|
|
|
167
167
|
nextPage: "Próxima página",
|
|
168
168
|
empty: "Nenhum registro",
|
|
169
169
|
emptyFiltered: "Nenhum registro para este filtro",
|
|
170
|
+
view: "Formato da listagem",
|
|
171
|
+
viewList: "Lista",
|
|
172
|
+
viewGrid: "Cartões",
|
|
170
173
|
reorder: "Arraste para reordenar",
|
|
171
174
|
reorderSorted:
|
|
172
175
|
"Limpe a ordenação por coluna para arrastar as linhas",
|
|
176
|
+
reorderGrouped: "Não dá para reordenar com a lista dividida em seções",
|
|
173
177
|
},
|
|
174
178
|
filters: {
|
|
175
179
|
button: "Filtros",
|
|
@@ -186,6 +190,7 @@ export const pt = {
|
|
|
186
190
|
toChip: "até",
|
|
187
191
|
selectPeriod: "Selecione o período",
|
|
188
192
|
selectDate: "Selecione a data",
|
|
193
|
+
openCalendar: "Abrir calendário",
|
|
189
194
|
today: "Hoje",
|
|
190
195
|
lastDays: "{days} dias",
|
|
191
196
|
previousMonth: "Mês anterior",
|
package/src/index.ts
CHANGED
|
@@ -110,6 +110,8 @@ export {
|
|
|
110
110
|
EXPAND_SHORTCUT_LABEL,
|
|
111
111
|
useShortcut,
|
|
112
112
|
} from "#core/hooks/use-shortcut";
|
|
113
|
+
export type { TableView, UseTableViewResult } from "#core/hooks/use-table-view";
|
|
114
|
+
export { useTableView } from "#core/hooks/use-table-view";
|
|
113
115
|
// RequestOperation é enum (valor, não só tipo): sem ele o projeto não consegue
|
|
114
116
|
// pedir o toast padrão de criado/alterado/removido que o core já traduz.
|
|
115
117
|
export { RequestOperation, useRequest } from "#core/hooks/use-request";
|