rl-core-front 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +34 -5
- package/package.json +1 -1
- package/src/components/app-shell.tsx +6 -6
- package/src/components/brand-header.tsx +35 -0
- package/src/components/index.ts +1 -0
- package/src/components/ui/data-table.tsx +12 -10
- package/src/contexts/brand-context.tsx +93 -0
- package/src/contexts/index.ts +1 -0
- package/src/features/login/login-screen.tsx +4 -8
- package/src/features/recovery/forgot-password-screen.tsx +2 -3
- package/src/features/recovery/reset-password-screen.tsx +2 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rodrigo Liberti
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -61,6 +61,7 @@ O `layout.tsx` empilha os providers:
|
|
|
61
61
|
```tsx
|
|
62
62
|
import {
|
|
63
63
|
AuthProvider,
|
|
64
|
+
BrandProvider,
|
|
64
65
|
ColorModeProvider,
|
|
65
66
|
I18nProvider,
|
|
66
67
|
SocketProvider,
|
|
@@ -71,17 +72,40 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
71
72
|
return (
|
|
72
73
|
<ColorModeProvider>
|
|
73
74
|
<I18nProvider>
|
|
74
|
-
<
|
|
75
|
-
<
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
<BrandProvider brand={{ name: "ControlX", mark: <Logo /> }}>
|
|
76
|
+
<ToastProvider>
|
|
77
|
+
<AuthProvider>
|
|
78
|
+
<SocketProvider>{children}</SocketProvider>
|
|
79
|
+
</AuthProvider>
|
|
80
|
+
</ToastProvider>
|
|
81
|
+
</BrandProvider>
|
|
79
82
|
</I18nProvider>
|
|
80
83
|
</ColorModeProvider>
|
|
81
84
|
);
|
|
82
85
|
}
|
|
83
86
|
```
|
|
84
87
|
|
|
88
|
+
## Identidade do sistema
|
|
89
|
+
|
|
90
|
+
O `BrandProvider` é o que troca o "Core App" pelo nome do projeto. Ele alimenta
|
|
91
|
+
o cabeçalho, o drawer e as telas de login, primeiro acesso e recuperação de
|
|
92
|
+
senha:
|
|
93
|
+
|
|
94
|
+
| Campo | O que é | Sem declarar |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `name` | Nome do sistema | a chave `app.name` das traduções |
|
|
97
|
+
| `mark` | Marca compacta do cabeçalho (~24px) | o escudo do core |
|
|
98
|
+
| `logo` | Versão maior, para as telas de login | cai na `mark` |
|
|
99
|
+
| `tagline` | Frase curta sob a marca no login | não aparece |
|
|
100
|
+
|
|
101
|
+
`mark` e `logo` são `ReactNode`, não URL — use SVG inline, `next/image` ou um
|
|
102
|
+
ícone, o que o projeto preferir. O provider inteiro é opcional: sem ele, o
|
|
103
|
+
sistema segue com o padrão do core.
|
|
104
|
+
|
|
105
|
+
As cores continuam vindo do `tailwind-preset` e do `styles.css`, e o backend tem
|
|
106
|
+
a identidade dele no `.env` (`APP_NAME`, `APP_LOGO_URL`, `APP_BRAND_COLOR`) — é
|
|
107
|
+
de lá que sai a marca dos emails e o nome no app autenticador.
|
|
108
|
+
|
|
85
109
|
E o `middleware.ts` da raiz:
|
|
86
110
|
|
|
87
111
|
```ts
|
|
@@ -124,6 +148,11 @@ export const messages = {
|
|
|
124
148
|
`next`, `react`, `react-dom` e `tailwindcss` — precisam ser uma instância só.
|
|
125
149
|
O npm 7+ instala sozinho.
|
|
126
150
|
|
|
151
|
+
## Changelog
|
|
152
|
+
|
|
153
|
+
As três versões andam juntas e saem de uma tag só — o que mudou em cada uma
|
|
154
|
+
está em [CHANGELOG.md](https://github.com/liberti1991/aplication-core/blob/main/CHANGELOG.md).
|
|
155
|
+
|
|
127
156
|
## Licença
|
|
128
157
|
|
|
129
158
|
MIT © Rodrigo Liberti
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rl-core-front",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
Menu,
|
|
10
10
|
ScrollText,
|
|
11
11
|
Settings,
|
|
12
|
-
Shield,
|
|
13
12
|
ShieldCheck,
|
|
14
13
|
User,
|
|
15
14
|
Users,
|
|
@@ -33,7 +32,7 @@ import {
|
|
|
33
32
|
Spinner,
|
|
34
33
|
} from "#core/components/ui";
|
|
35
34
|
import { UserAvatar } from "#core/components/user-avatar";
|
|
36
|
-
import { useAuth, useI18n } from "#core/contexts";
|
|
35
|
+
import { useAuth, useBrand, useI18n } from "#core/contexts";
|
|
37
36
|
import { NotificationsCenter } from "#core/features/notifications/notifications-center";
|
|
38
37
|
import { ForcePasswordChange } from "#core/features/profile/force-password-change";
|
|
39
38
|
import { cn } from "#core/lib/utils";
|
|
@@ -75,6 +74,7 @@ const ADMIN_CHILDREN: NavItem[] = [
|
|
|
75
74
|
export function AppShell({ children }: { children: React.ReactNode }): JSX.Element {
|
|
76
75
|
const { user, loading, logout, hasPermission } = useAuth();
|
|
77
76
|
const { t } = useI18n();
|
|
77
|
+
const brand = useBrand();
|
|
78
78
|
const router = useRouter();
|
|
79
79
|
const pathname = usePathname();
|
|
80
80
|
|
|
@@ -221,9 +221,9 @@ export function AppShell({ children }: { children: React.ReactNode }): JSX.Eleme
|
|
|
221
221
|
</Button>
|
|
222
222
|
|
|
223
223
|
<div className="flex items-center gap-2">
|
|
224
|
-
|
|
224
|
+
{brand.mark}
|
|
225
225
|
<span className="hidden text-lg font-extrabold sm:block">
|
|
226
|
-
{
|
|
226
|
+
{brand.name}
|
|
227
227
|
</span>
|
|
228
228
|
</div>
|
|
229
229
|
|
|
@@ -289,8 +289,8 @@ export function AppShell({ children }: { children: React.ReactNode }): JSX.Eleme
|
|
|
289
289
|
/>
|
|
290
290
|
<aside className="absolute left-0 top-0 flex h-full w-64 flex-col border-r border-border bg-card">
|
|
291
291
|
<div className="flex h-16 items-center gap-2 border-b border-border px-4">
|
|
292
|
-
|
|
293
|
-
<span className="text-lg font-extrabold">{
|
|
292
|
+
{brand.mark}
|
|
293
|
+
<span className="text-lg font-extrabold">{brand.name}</span>
|
|
294
294
|
</div>
|
|
295
295
|
<NavContent mini={false} />
|
|
296
296
|
</aside>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { JSX } from "react";
|
|
4
|
+
|
|
5
|
+
import { useBrand } from "#core/contexts";
|
|
6
|
+
|
|
7
|
+
export interface BrandHeaderProps {
|
|
8
|
+
/** Título do passo ou da tela, exibido sob a marca. */
|
|
9
|
+
title: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Cabeçalho das telas de autenticação: logo, nome do sistema e o título da vez.
|
|
14
|
+
*
|
|
15
|
+
* Existe porque login, primeiro acesso e recuperação de senha são as três telas
|
|
16
|
+
* que uma pessoa vê antes de entrar — e as três precisam dizer em que sistema
|
|
17
|
+
* ela está. O conteúdo vem do `BrandProvider` do projeto; sem ele, do padrão do
|
|
18
|
+
* core.
|
|
19
|
+
*/
|
|
20
|
+
export function BrandHeader({ title }: BrandHeaderProps): JSX.Element {
|
|
21
|
+
const brand = useBrand();
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="mb-6 flex flex-col items-center gap-1">
|
|
25
|
+
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
|
|
26
|
+
{brand.logo}
|
|
27
|
+
</div>
|
|
28
|
+
<span className="text-lg font-extrabold">{brand.name}</span>
|
|
29
|
+
{brand.tagline && (
|
|
30
|
+
<p className="text-sm text-muted-foreground">{brand.tagline}</p>
|
|
31
|
+
)}
|
|
32
|
+
<h1 className="mt-2 text-xl font-semibold">{title}</h1>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -227,16 +227,8 @@ export function DataTable<T extends RowData>({
|
|
|
227
227
|
<div className="mb-3 flex items-center justify-between gap-2">
|
|
228
228
|
<div className="flex items-center gap-2">{toolbar}</div>
|
|
229
229
|
<div className="flex items-center gap-2">
|
|
230
|
-
{
|
|
231
|
-
|
|
232
|
-
variant="outline"
|
|
233
|
-
size="sm"
|
|
234
|
-
onClick={() => setColsOpen(true)}
|
|
235
|
-
>
|
|
236
|
-
<SlidersHorizontal className="h-4 w-4" />
|
|
237
|
-
{t("table.columnsButton")}
|
|
238
|
-
</Button>
|
|
239
|
-
)}
|
|
230
|
+
{/* Filtro antes de colunas: filtrar é a ação do dia a dia, escolher
|
|
231
|
+
coluna se faz uma vez e a tela guarda. */}
|
|
240
232
|
{filtersEnabled && (
|
|
241
233
|
<Button
|
|
242
234
|
variant="outline"
|
|
@@ -252,6 +244,16 @@ export function DataTable<T extends RowData>({
|
|
|
252
244
|
)}
|
|
253
245
|
</Button>
|
|
254
246
|
)}
|
|
247
|
+
{storageKey && hideable.length > 0 && (
|
|
248
|
+
<Button
|
|
249
|
+
variant="outline"
|
|
250
|
+
size="sm"
|
|
251
|
+
onClick={() => setColsOpen(true)}
|
|
252
|
+
>
|
|
253
|
+
<SlidersHorizontal className="h-4 w-4" />
|
|
254
|
+
{t("table.columnsButton")}
|
|
255
|
+
</Button>
|
|
256
|
+
)}
|
|
255
257
|
</div>
|
|
256
258
|
</div>
|
|
257
259
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Shield } from "lucide-react";
|
|
4
|
+
import type { JSX, ReactNode } from "react";
|
|
5
|
+
import { createContext, useContext, useMemo } from "react";
|
|
6
|
+
|
|
7
|
+
import { useI18n } from "#core/contexts/i18n-context";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Identidade visual do sistema.
|
|
11
|
+
*
|
|
12
|
+
* O core não tem nome nem logo próprios: quem se chama de alguma coisa é o
|
|
13
|
+
* projeto que o consome. Declarar é opcional — sem `BrandProvider`, o shell e o
|
|
14
|
+
* login seguem com o escudo e o `app.name` das traduções, que é o que já
|
|
15
|
+
* acontecia antes desta configuração existir.
|
|
16
|
+
*
|
|
17
|
+
* A logo entra como `ReactNode` e não como URL de propósito: assim o projeto
|
|
18
|
+
* usa o que quiser (SVG inline, `next/image`, um ícone da lucide) sem que o
|
|
19
|
+
* core precise saber de onde o arquivo vem.
|
|
20
|
+
*/
|
|
21
|
+
export interface Brand {
|
|
22
|
+
/** Nome do sistema. Sem isto vale a chave `app.name` das traduções. */
|
|
23
|
+
name?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Marca compacta, exibida ao lado do nome no cabeçalho e no drawer.
|
|
26
|
+
* Deve caber num quadrado de ~24px. Sem isto vale o escudo do core.
|
|
27
|
+
*/
|
|
28
|
+
mark?: ReactNode;
|
|
29
|
+
/**
|
|
30
|
+
* Versão maior, para as telas de login, primeiro acesso e recuperação de
|
|
31
|
+
* senha. Sem isto a `mark` é usada nos dois lugares.
|
|
32
|
+
*/
|
|
33
|
+
logo?: ReactNode;
|
|
34
|
+
/** Frase curta sob a logo na tela de login. Opcional. */
|
|
35
|
+
tagline?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** O que os componentes do core consomem: sem opcionais, tudo já resolvido. */
|
|
39
|
+
export interface BrandContextType {
|
|
40
|
+
name: string;
|
|
41
|
+
mark: ReactNode;
|
|
42
|
+
logo: ReactNode;
|
|
43
|
+
tagline: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const BrandContext = createContext<BrandContextType | undefined>(undefined);
|
|
47
|
+
|
|
48
|
+
/** Marca do core: é o que aparece enquanto o projeto não declara a dele. */
|
|
49
|
+
const defaultMark = <Shield className="h-6 w-6 text-primary" />;
|
|
50
|
+
|
|
51
|
+
export function BrandProvider({
|
|
52
|
+
brand,
|
|
53
|
+
children,
|
|
54
|
+
}: {
|
|
55
|
+
brand?: Brand;
|
|
56
|
+
children: ReactNode;
|
|
57
|
+
}): JSX.Element {
|
|
58
|
+
const { t } = useI18n();
|
|
59
|
+
|
|
60
|
+
const value = useMemo<BrandContextType>(() => {
|
|
61
|
+
const mark = brand?.mark ?? defaultMark;
|
|
62
|
+
return {
|
|
63
|
+
name: brand?.name ?? t("app.name"),
|
|
64
|
+
mark,
|
|
65
|
+
logo: brand?.logo ?? mark,
|
|
66
|
+
tagline: brand?.tagline ?? null,
|
|
67
|
+
};
|
|
68
|
+
}, [brand, t]);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<BrandContext.Provider value={value}>{children}</BrandContext.Provider>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Identidade em vigor.
|
|
77
|
+
*
|
|
78
|
+
* Funciona mesmo sem `BrandProvider` no topo: cai no padrão do core em vez de
|
|
79
|
+
* quebrar, para que um projeto que ainda não declarou marca continue subindo.
|
|
80
|
+
*/
|
|
81
|
+
export function useBrand(): BrandContextType {
|
|
82
|
+
const ctx = useContext(BrandContext);
|
|
83
|
+
const { t } = useI18n();
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
ctx ?? {
|
|
87
|
+
name: t("app.name"),
|
|
88
|
+
mark: defaultMark,
|
|
89
|
+
logo: defaultMark,
|
|
90
|
+
tagline: null,
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
}
|
package/src/contexts/index.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { Lock } from "lucide-react";
|
|
4
3
|
import type { JSX } from "react";
|
|
5
4
|
import { useEffect } from "react";
|
|
6
5
|
|
|
7
|
-
import { LanguageSelector, ThemeToggle } from "#core/components";
|
|
6
|
+
import { BrandHeader, LanguageSelector, ThemeToggle } from "#core/components";
|
|
8
7
|
import { Card } from "#core/components/ui";
|
|
9
8
|
import { useI18n } from "#core/contexts";
|
|
10
9
|
import {
|
|
@@ -39,12 +38,9 @@ export function LoginScreen(): JSX.Element {
|
|
|
39
38
|
</div>
|
|
40
39
|
|
|
41
40
|
<Card className="w-full max-w-md p-8">
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
</div>
|
|
46
|
-
<h1 className="text-xl font-semibold">{titles[flow.step]}</h1>
|
|
47
|
-
</div>
|
|
41
|
+
{/* A marca vem antes do título do passo: é a primeira tela do sistema,
|
|
42
|
+
e quem chega aqui precisa reconhecer onde está antes de logar. */}
|
|
43
|
+
<BrandHeader title={titles[flow.step]} />
|
|
48
44
|
|
|
49
45
|
{flow.error && (
|
|
50
46
|
<div className="mb-4 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
|
@@ -8,6 +8,7 @@ import { useForm } from "react-hook-form";
|
|
|
8
8
|
import * as yup from "yup";
|
|
9
9
|
|
|
10
10
|
import { authService } from "#core/_services/auth";
|
|
11
|
+
import { BrandHeader } from "#core/components";
|
|
11
12
|
import { Alert, Button, Card, Field, Input } from "#core/components/ui";
|
|
12
13
|
import { useI18n } from "#core/contexts";
|
|
13
14
|
import { useRequest } from "#core/hooks/use-request";
|
|
@@ -36,9 +37,7 @@ export function ForgotPasswordScreen(): JSX.Element {
|
|
|
36
37
|
return (
|
|
37
38
|
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
|
38
39
|
<Card className="w-full max-w-md p-8">
|
|
39
|
-
<
|
|
40
|
-
{t("recovery.forgotTitle")}
|
|
41
|
-
</h1>
|
|
40
|
+
<BrandHeader title={t("recovery.forgotTitle")} />
|
|
42
41
|
{sent ? (
|
|
43
42
|
<Alert variant="success">{t("recovery.forgotSuccess")}</Alert>
|
|
44
43
|
) : (
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
PasswordTokenStatus,
|
|
14
14
|
} from "#core/_services/auth";
|
|
15
15
|
import { STRONG_PASSWORD_REGEX } from "#core/_utils/password";
|
|
16
|
-
import { PasswordField } from "#core/components";
|
|
16
|
+
import { BrandHeader, PasswordField } from "#core/components";
|
|
17
17
|
import { Alert, Button, Card, Field, Spinner } from "#core/components/ui";
|
|
18
18
|
import { useI18n } from "#core/contexts";
|
|
19
19
|
import { useRequest } from "#core/hooks/use-request";
|
|
@@ -92,7 +92,7 @@ export function ResetPasswordScreen(): JSX.Element {
|
|
|
92
92
|
return (
|
|
93
93
|
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
|
94
94
|
<Card className="w-full max-w-md p-8">
|
|
95
|
-
<
|
|
95
|
+
<BrandHeader title={title} />
|
|
96
96
|
|
|
97
97
|
{checking && (
|
|
98
98
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|