rl-core-front 0.11.2 → 0.11.4
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rl-core-front",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.4",
|
|
4
4
|
"description": "Telas e componentes Next.js do core: login com 2FA, usu\u00e1rios, RBAC, auditoria, logs e listagens com filtro din\u00e2mico",
|
|
5
5
|
"author": "Rodrigo Liberti",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Um item da barra de páginas: o número da página, ou o buraco entre elas. */
|
|
2
|
+
export type PageItem = number | "gap";
|
|
3
|
+
|
|
4
|
+
/** Quantas páginas cabem sem precisar esconder nenhuma. */
|
|
5
|
+
const SEM_BURACO = 7;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A régua de páginas do rodapé: `1 2 3 4 … 18`.
|
|
9
|
+
*
|
|
10
|
+
* Primeira e última sempre aparecem — são os dois saltos que mais se usam —, e
|
|
11
|
+
* no meio fica a vizinhança de onde a pessoa está. Com poucas páginas não há o
|
|
12
|
+
* que esconder e todas saem; daí em diante a lista tem largura fixa, para o
|
|
13
|
+
* rodapé não mudar de tamanho a cada clique.
|
|
14
|
+
*
|
|
15
|
+
* `pageIndex` é o índice do TanStack (base zero); o que sai já é número de
|
|
16
|
+
* página (base um), que é o que se lê na tela.
|
|
17
|
+
*/
|
|
18
|
+
export const pageRange = (pageIndex: number, pageCount: number): PageItem[] => {
|
|
19
|
+
const total = Math.max(1, pageCount);
|
|
20
|
+
const atual = Math.min(Math.max(1, pageIndex + 1), total);
|
|
21
|
+
|
|
22
|
+
if (total <= SEM_BURACO) {
|
|
23
|
+
return Array.from({ length: total }, (_, i) => i + 1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Nas pontas, a janela se estende para o lado que tem páginas: perto do
|
|
27
|
+
// começo, mostrar "… 2 3 4 …" desperdiçaria o espaço da esquerda.
|
|
28
|
+
if (atual <= 3) {
|
|
29
|
+
return [1, 2, 3, 4, "gap", total];
|
|
30
|
+
}
|
|
31
|
+
if (atual >= total - 2) {
|
|
32
|
+
return [1, "gap", total - 3, total - 2, total - 1, total];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return [1, "gap", atual - 1, atual, atual + 1, "gap", total];
|
|
36
|
+
};
|
|
@@ -33,6 +33,7 @@ import { useMemo, useState } from "react";
|
|
|
33
33
|
|
|
34
34
|
import { countConditions } from "#core/_utils/advanced-filter";
|
|
35
35
|
import { activeFilterCount } from "#core/_utils/filter";
|
|
36
|
+
import { pageRange } from "#core/_utils/page-range";
|
|
36
37
|
import { Button } from "#core/components/ui/button";
|
|
37
38
|
import { Card } from "#core/components/ui/card";
|
|
38
39
|
import {
|
|
@@ -251,9 +252,20 @@ export function DataTable<T extends RowData>({
|
|
|
251
252
|
|
|
252
253
|
return (
|
|
253
254
|
<div>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
255
|
+
{/*
|
|
256
|
+
A barra quebra em vez de empurrar a página.
|
|
257
|
+
|
|
258
|
+
Sem `flex-wrap`, uma busca de 288px ao lado de "Novo" + "Filtros" +
|
|
259
|
+
"Colunas" passava da largura do celular, e quem rolava para o lado era o
|
|
260
|
+
documento inteiro: a tabela saía da tela com o cabeçalho junto. O
|
|
261
|
+
`min-w-0` é o par obrigatório disso — filho de flex não encolhe abaixo
|
|
262
|
+
do próprio conteúdo sem ele, e o `flex-wrap` sozinho não bastaria.
|
|
263
|
+
*/}
|
|
264
|
+
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
|
265
|
+
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
266
|
+
{toolbar}
|
|
267
|
+
</div>
|
|
268
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
257
269
|
{/* Filtro antes de colunas: filtrar é a ação do dia a dia, escolher
|
|
258
270
|
coluna se faz uma vez e a tela guarda. */}
|
|
259
271
|
{filtersEnabled && (
|
|
@@ -366,44 +378,78 @@ export function DataTable<T extends RowData>({
|
|
|
366
378
|
</TableBody>
|
|
367
379
|
</Table>
|
|
368
380
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
</select>
|
|
382
|
-
<span>
|
|
383
|
-
{total} {t("table.results")}
|
|
384
|
-
</span>
|
|
385
|
-
</div>
|
|
386
|
-
<div className="flex items-center gap-2">
|
|
381
|
+
{/*
|
|
382
|
+
Três blocos, e a ordem tem motivo: quantos são à esquerda, para onde
|
|
383
|
+
ir no meio, e o tamanho da página à direita — o controle que se mexe
|
|
384
|
+
uma vez fica na ponta, e a navegação, que se usa o tempo todo, no
|
|
385
|
+
centro. Em tela estreita eles empilham em vez de espremer.
|
|
386
|
+
*/}
|
|
387
|
+
<div className="flex flex-col items-center gap-3 border-t border-border px-4 py-3 text-sm text-muted-foreground sm:flex-row sm:justify-between">
|
|
388
|
+
<span className="sm:flex-1">
|
|
389
|
+
{total} {t("table.results")}
|
|
390
|
+
</span>
|
|
391
|
+
|
|
392
|
+
<div className="flex items-center gap-1">
|
|
387
393
|
<Button
|
|
388
|
-
variant="
|
|
394
|
+
variant="ghost"
|
|
389
395
|
size="icon"
|
|
396
|
+
aria-label={t("table.previousPage")}
|
|
390
397
|
disabled={!table.getCanPreviousPage()}
|
|
391
398
|
onClick={() => table.previousPage()}
|
|
392
399
|
>
|
|
393
400
|
<ChevronLeft className="h-4 w-4" />
|
|
394
401
|
</Button>
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
402
|
+
|
|
403
|
+
{pageRange(pageIndex, pageCount).map((item, posicao) =>
|
|
404
|
+
item === "gap" ? (
|
|
405
|
+
// O buraco não é botão: `key` pela posição porque duas elipses
|
|
406
|
+
// são indistinguíveis entre si.
|
|
407
|
+
<span
|
|
408
|
+
key={`gap-${posicao}`}
|
|
409
|
+
className="px-1 text-muted-foreground"
|
|
410
|
+
>
|
|
411
|
+
…
|
|
412
|
+
</span>
|
|
413
|
+
) : (
|
|
414
|
+
<Button
|
|
415
|
+
key={item}
|
|
416
|
+
variant={item === pageIndex + 1 ? "default" : "ghost"}
|
|
417
|
+
size="icon"
|
|
418
|
+
// Quadrado de 40px como base, mas cresce com o número: a
|
|
419
|
+
// página 128 não pode vazar do botão.
|
|
420
|
+
className="w-auto min-w-10 px-2 tabular-nums"
|
|
421
|
+
aria-current={item === pageIndex + 1 ? "page" : undefined}
|
|
422
|
+
onClick={() => table.setPageIndex(item - 1)}
|
|
423
|
+
>
|
|
424
|
+
{item}
|
|
425
|
+
</Button>
|
|
426
|
+
),
|
|
427
|
+
)}
|
|
428
|
+
|
|
398
429
|
<Button
|
|
399
|
-
variant="
|
|
430
|
+
variant="ghost"
|
|
400
431
|
size="icon"
|
|
432
|
+
aria-label={t("table.nextPage")}
|
|
401
433
|
disabled={!table.getCanNextPage()}
|
|
402
434
|
onClick={() => table.nextPage()}
|
|
403
435
|
>
|
|
404
436
|
<ChevronRight className="h-4 w-4" />
|
|
405
437
|
</Button>
|
|
406
438
|
</div>
|
|
439
|
+
|
|
440
|
+
<div className="sm:flex-1 sm:text-right">
|
|
441
|
+
<select
|
|
442
|
+
value={pageSize}
|
|
443
|
+
onChange={(e) => table.setPageSize(Number(e.target.value))}
|
|
444
|
+
className="h-9 rounded-md border border-input bg-transparent px-2 text-sm"
|
|
445
|
+
>
|
|
446
|
+
{pageSizeOptions.map((n) => (
|
|
447
|
+
<option key={n} value={n}>
|
|
448
|
+
{n} {t("table.perPage")}
|
|
449
|
+
</option>
|
|
450
|
+
))}
|
|
451
|
+
</select>
|
|
452
|
+
</div>
|
|
407
453
|
</div>
|
|
408
454
|
</Card>
|
|
409
455
|
|
|
@@ -15,7 +15,16 @@ const TabsList = React.forwardRef<
|
|
|
15
15
|
>(({ className, ...props }, ref) => (
|
|
16
16
|
<TabsPrimitive.List
|
|
17
17
|
ref={ref}
|
|
18
|
-
|
|
18
|
+
/*
|
|
19
|
+
* Seis abas não cabem num celular, e a fila inteira empurrava a página para
|
|
20
|
+
* o lado. Agora quem rola é a própria fila: `max-w-full` para ela parar na
|
|
21
|
+
* largura da tela, e `overflow-x-auto` para o resto continuar alcançável.
|
|
22
|
+
*/
|
|
23
|
+
className={cn(
|
|
24
|
+
"inline-flex max-w-full overflow-x-auto",
|
|
25
|
+
TRACK_CLASS,
|
|
26
|
+
className,
|
|
27
|
+
)}
|
|
19
28
|
{...props}
|
|
20
29
|
/>
|
|
21
30
|
));
|
|
@@ -20,7 +20,9 @@ export const TRACK_CLASS =
|
|
|
20
20
|
* opção selecionada sumir no tema escuro.
|
|
21
21
|
*/
|
|
22
22
|
export const TRACK_ITEM_CLASS = [
|
|
23
|
-
|
|
23
|
+
// `shrink-0`: numa trilha que rola, a opção mantém o tamanho em vez de ser
|
|
24
|
+
// espremida até o texto virar duas letras.
|
|
25
|
+
"inline-flex shrink-0 items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5",
|
|
24
26
|
"text-sm font-medium ring-offset-background transition-all",
|
|
25
27
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
|
|
26
28
|
"disabled:pointer-events-none disabled:opacity-50",
|
package/src/i18n/messages/en.ts
CHANGED
|
@@ -118,6 +118,8 @@ export const en: Messages = {
|
|
|
118
118
|
atLeastOne: "At least one column must remain visible",
|
|
119
119
|
results: "results",
|
|
120
120
|
perPage: "per page",
|
|
121
|
+
previousPage: "Previous page",
|
|
122
|
+
nextPage: "Next page",
|
|
121
123
|
empty: "No records",
|
|
122
124
|
emptyFiltered: "No records match this filter",
|
|
123
125
|
},
|
package/src/i18n/messages/pt.ts
CHANGED
|
@@ -120,6 +120,8 @@ export const pt = {
|
|
|
120
120
|
atLeastOne: "Pelo menos uma coluna deve permanecer visível",
|
|
121
121
|
results: "resultados",
|
|
122
122
|
perPage: "por página",
|
|
123
|
+
previousPage: "Página anterior",
|
|
124
|
+
nextPage: "Próxima página",
|
|
123
125
|
empty: "Nenhum registro",
|
|
124
126
|
emptyFiltered: "Nenhum registro para este filtro",
|
|
125
127
|
},
|