rl-core-front 0.13.1 → 0.14.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rl-core-front",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
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,65 @@
|
|
|
1
|
+
/** Um degrau da migalha. Sem `href` quando não há para onde ir. */
|
|
2
|
+
export interface BreadcrumbStep {
|
|
3
|
+
label: string;
|
|
4
|
+
href?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** O que a migalha precisa saber de um item de menu. */
|
|
8
|
+
export interface BreadcrumbNavItem {
|
|
9
|
+
label: string;
|
|
10
|
+
href?: string;
|
|
11
|
+
children?: BreadcrumbNavItem[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A trilha do caminho atual, lida do **menu** e não da URL.
|
|
16
|
+
*
|
|
17
|
+
* A URL é onde a tela mora; o menu é onde a pessoa acha que está. Os dois
|
|
18
|
+
* combinam na maioria das telas e divergem justamente nas reorganizadas: uma
|
|
19
|
+
* tela em `Cadastros › Funkos` pode morar em `/funkos/catalogos`, e a migalha
|
|
20
|
+
* por URL mostrava "funkos › catalogos" — dois nomes que não estão em lugar
|
|
21
|
+
* nenhum do menu.
|
|
22
|
+
*
|
|
23
|
+
* Rota fora do menu (link direto, detalhe de um registro) cai nos segmentos da
|
|
24
|
+
* URL: melhor um nome cru do que nenhum rastro.
|
|
25
|
+
*
|
|
26
|
+
* Grupo que só abre e fecha entra sem `href` — vira texto, e não link para uma
|
|
27
|
+
* rota que não existe.
|
|
28
|
+
*/
|
|
29
|
+
export const breadcrumbTrail = (
|
|
30
|
+
pathname: string,
|
|
31
|
+
navItems: readonly BreadcrumbNavItem[] | undefined,
|
|
32
|
+
segmentLabels: Record<string, string> = {},
|
|
33
|
+
): BreadcrumbStep[] => {
|
|
34
|
+
const fromMenu = findInMenu(pathname, navItems ?? []);
|
|
35
|
+
if (fromMenu) {
|
|
36
|
+
return fromMenu;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
40
|
+
return segments.map((segment, index) => ({
|
|
41
|
+
label: segmentLabels[segment] ?? capitalize(segment),
|
|
42
|
+
href: "/" + segments.slice(0, index + 1).join("/"),
|
|
43
|
+
}));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Busca em profundidade: o primeiro item com o `href` pedido ganha. */
|
|
47
|
+
const findInMenu = (
|
|
48
|
+
pathname: string,
|
|
49
|
+
items: readonly BreadcrumbNavItem[],
|
|
50
|
+
): BreadcrumbStep[] | null => {
|
|
51
|
+
for (const item of items) {
|
|
52
|
+
if (item.href === pathname) {
|
|
53
|
+
return [{ label: item.label, href: item.href }];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const below = findInMenu(pathname, item.children ?? []);
|
|
57
|
+
if (below) {
|
|
58
|
+
return [{ label: item.label, href: item.href }, ...below];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const capitalize = (text: string): string =>
|
|
65
|
+
text.charAt(0).toUpperCase() + text.slice(1);
|
|
@@ -557,7 +557,7 @@ export function AppShell({
|
|
|
557
557
|
)}
|
|
558
558
|
>
|
|
559
559
|
<div className="p-4 md:p-8">
|
|
560
|
-
{!routeBlocked && <Breadcrumbs />}
|
|
560
|
+
{!routeBlocked && <Breadcrumbs navItems={navItems} />}
|
|
561
561
|
{routeBlocked ? (forbidden ?? <ForbiddenScreen />) : children}
|
|
562
562
|
</div>
|
|
563
563
|
</main>
|
|
@@ -4,8 +4,18 @@ import { ChevronRight, Home } from "lucide-react";
|
|
|
4
4
|
import { usePathname, useRouter } from "next/navigation";
|
|
5
5
|
import type { JSX } from "react";
|
|
6
6
|
|
|
7
|
+
import {
|
|
8
|
+
type BreadcrumbNavItem,
|
|
9
|
+
breadcrumbTrail,
|
|
10
|
+
} from "#core/_utils/breadcrumb-trail";
|
|
7
11
|
import { useI18n } from "#core/contexts";
|
|
8
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Rótulo das telas do próprio core — o projeto não precisa cadastrá-las.
|
|
15
|
+
*
|
|
16
|
+
* Vale só para o caminho de exceção: tela que está no menu tem o rótulo do
|
|
17
|
+
* menu, que é o que a pessoa acabou de clicar.
|
|
18
|
+
*/
|
|
9
19
|
const SEGMENT_LABELS: Record<string, string> = {
|
|
10
20
|
dashboard: "nav.dashboard",
|
|
11
21
|
usuarios: "nav.users",
|
|
@@ -14,17 +24,17 @@ const SEGMENT_LABELS: Record<string, string> = {
|
|
|
14
24
|
perfil: "nav.profile",
|
|
15
25
|
};
|
|
16
26
|
|
|
17
|
-
export
|
|
27
|
+
export interface BreadcrumbsProps {
|
|
28
|
+
/** O mesmo menu do `AppShell`: é dele que sai a trilha. */
|
|
29
|
+
navItems?: BreadcrumbNavItem[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function Breadcrumbs({ navItems }: BreadcrumbsProps): JSX.Element {
|
|
18
33
|
const pathname = usePathname();
|
|
19
34
|
const router = useRouter();
|
|
20
35
|
const { t } = useI18n();
|
|
21
36
|
|
|
22
|
-
const
|
|
23
|
-
const crumbs = segments.map((seg, i) => ({
|
|
24
|
-
label: SEGMENT_LABELS[seg] ? t(SEGMENT_LABELS[seg]) : seg,
|
|
25
|
-
href: "/" + segments.slice(0, i + 1).join("/"),
|
|
26
|
-
isLast: i === segments.length - 1,
|
|
27
|
-
}));
|
|
37
|
+
const trail = breadcrumbTrail(pathname, navItems, SEGMENT_LABELS);
|
|
28
38
|
|
|
29
39
|
return (
|
|
30
40
|
<nav className="mb-4 flex items-center gap-1.5 text-sm text-muted-foreground">
|
|
@@ -35,21 +45,32 @@ export function Breadcrumbs(): JSX.Element {
|
|
|
35
45
|
<Home className="h-4 w-4" />
|
|
36
46
|
{t("nav.home")}
|
|
37
47
|
</button>
|
|
38
|
-
{
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
{
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
{trail.map((step, index) => {
|
|
49
|
+
const isLast = index === trail.length - 1;
|
|
50
|
+
return (
|
|
51
|
+
<span key={`${step.href ?? step.label}`} className="flex items-center gap-1.5">
|
|
52
|
+
<ChevronRight className="h-4 w-4 text-muted-foreground/50" />
|
|
53
|
+
{isLast || !step.href ? (
|
|
54
|
+
// Sem `href` é grupo do menu: abre e fecha, não navega. Virar
|
|
55
|
+
// botão aqui prometia uma tela que não existe.
|
|
56
|
+
<span
|
|
57
|
+
className={
|
|
58
|
+
isLast ? "font-semibold text-foreground" : undefined
|
|
59
|
+
}
|
|
60
|
+
>
|
|
61
|
+
{t(step.label)}
|
|
62
|
+
</span>
|
|
63
|
+
) : (
|
|
64
|
+
<button
|
|
65
|
+
onClick={() => router.push(step.href as string)}
|
|
66
|
+
className="transition-colors hover:text-foreground"
|
|
67
|
+
>
|
|
68
|
+
{t(step.label)}
|
|
69
|
+
</button>
|
|
70
|
+
)}
|
|
71
|
+
</span>
|
|
72
|
+
);
|
|
73
|
+
})}
|
|
53
74
|
</nav>
|
|
54
75
|
);
|
|
55
76
|
}
|