rl-core-front 0.1.0 → 0.3.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 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
- <ToastProvider>
75
- <AuthProvider>
76
- <SocketProvider>{children}</SocketProvider>
77
- </AuthProvider>
78
- </ToastProvider>
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.1.0",
3
+ "version": "0.3.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",
@@ -929,6 +929,20 @@ export interface components {
929
929
  isActive: boolean;
930
930
  roleIds?: string[];
931
931
  };
932
+ ResourceResponse: {
933
+ /** Format: uuid */
934
+ id: string;
935
+ /** Format: date-time */
936
+ createdAt: string;
937
+ /** Format: date-time */
938
+ updatedAt: string;
939
+ /** Format: date-time */
940
+ deletedAt?: string | null;
941
+ /** @example users */
942
+ name: string;
943
+ description?: string | null;
944
+ isActive: boolean;
945
+ };
932
946
  CreateResourceCommand: {
933
947
  /** @example users */
934
948
  name: string;
@@ -940,6 +954,20 @@ export interface components {
940
954
  description?: string;
941
955
  isActive?: boolean;
942
956
  };
957
+ ActionResponse: {
958
+ /** Format: uuid */
959
+ id: string;
960
+ /** Format: date-time */
961
+ createdAt: string;
962
+ /** Format: date-time */
963
+ updatedAt: string;
964
+ /** Format: date-time */
965
+ deletedAt?: string | null;
966
+ /** @example read */
967
+ name: string;
968
+ description?: string | null;
969
+ isActive: boolean;
970
+ };
943
971
  CreateActionCommand: {
944
972
  /** @example read */
945
973
  name: string;
@@ -951,6 +979,20 @@ export interface components {
951
979
  description?: string;
952
980
  isActive?: boolean;
953
981
  };
982
+ ScopeResponse: {
983
+ /** Format: uuid */
984
+ id: string;
985
+ /** Format: date-time */
986
+ createdAt: string;
987
+ /** Format: date-time */
988
+ updatedAt: string;
989
+ /** Format: date-time */
990
+ deletedAt?: string | null;
991
+ /** @example any */
992
+ name: string;
993
+ description?: string | null;
994
+ isActive: boolean;
995
+ };
954
996
  CreateScopeCommand: {
955
997
  /**
956
998
  * @description own | team | department | any
@@ -968,6 +1010,29 @@ export interface components {
968
1010
  description?: string;
969
1011
  isActive?: boolean;
970
1012
  };
1013
+ PermissionResponse: {
1014
+ /** Format: uuid */
1015
+ id: string;
1016
+ /** Format: date-time */
1017
+ createdAt: string;
1018
+ /** Format: date-time */
1019
+ updatedAt: string;
1020
+ /** Format: date-time */
1021
+ deletedAt?: string | null;
1022
+ /** @example users:read:any */
1023
+ code: string;
1024
+ description?: string | null;
1025
+ isActive: boolean;
1026
+ /** Format: uuid */
1027
+ resourceId: string;
1028
+ /** Format: uuid */
1029
+ actionId: string;
1030
+ /** Format: uuid */
1031
+ scopeId: string;
1032
+ resource: components["schemas"]["ResourceResponse"];
1033
+ action: components["schemas"]["ActionResponse"];
1034
+ scope: components["schemas"]["ScopeResponse"];
1035
+ };
971
1036
  CreatePermissionCommand: {
972
1037
  /**
973
1038
  * @description resource:action:scope
@@ -985,6 +1050,40 @@ export interface components {
985
1050
  description?: string;
986
1051
  isActive?: boolean;
987
1052
  };
1053
+ /** @enum {string} */
1054
+ GroupType: "company" | "department" | "team";
1055
+ RoleResponse: {
1056
+ /** Format: uuid */
1057
+ id: string;
1058
+ /** Format: date-time */
1059
+ createdAt: string;
1060
+ /** Format: date-time */
1061
+ updatedAt: string;
1062
+ /** Format: date-time */
1063
+ deletedAt?: string | null;
1064
+ /** @example admin */
1065
+ name: string;
1066
+ description?: string | null;
1067
+ isActive: boolean;
1068
+ permissions?: components["schemas"]["PermissionResponse"][];
1069
+ };
1070
+ GroupResponse: {
1071
+ /** Format: uuid */
1072
+ id: string;
1073
+ /** Format: date-time */
1074
+ createdAt: string;
1075
+ /** Format: date-time */
1076
+ updatedAt: string;
1077
+ /** Format: date-time */
1078
+ deletedAt?: string | null;
1079
+ name: string;
1080
+ description?: string | null;
1081
+ type: components["schemas"]["GroupType"];
1082
+ /** Format: uuid */
1083
+ parentId?: string | null;
1084
+ isActive: boolean;
1085
+ roles?: components["schemas"]["RoleResponse"][];
1086
+ };
988
1087
  CreateGroupCommand: {
989
1088
  name: string;
990
1089
  description?: string;
@@ -1023,8 +1122,6 @@ export interface components {
1023
1122
  permissionIds?: string[];
1024
1123
  isActive?: boolean;
1025
1124
  };
1026
- /** @enum {string} */
1027
- AuditDataAction: "CREATE" | "UPDATE" | "DELETE";
1028
1125
  AuditUserRefResponse: {
1029
1126
  /** Format: uuid */
1030
1127
  id: string;
@@ -1032,6 +1129,71 @@ export interface components {
1032
1129
  /** @example admin@admin.com */
1033
1130
  email: string;
1034
1131
  };
1132
+ RequestLogResponse: {
1133
+ /** Format: uuid */
1134
+ id: string;
1135
+ /** @example GET /api/v1/users */
1136
+ action: string;
1137
+ /** @example User */
1138
+ entity?: string | null;
1139
+ entityId?: string | null;
1140
+ /** @example GET */
1141
+ method?: string | null;
1142
+ path?: string | null;
1143
+ /** @example 200 */
1144
+ statusCode?: number | null;
1145
+ /** @example 42 */
1146
+ durationMs?: number | null;
1147
+ /** @example 10.0.0.1 */
1148
+ ipAddress?: string | null;
1149
+ /** Format: uuid */
1150
+ requestId?: string | null;
1151
+ metadata?: Record<string, never> | null;
1152
+ userId?: string | null;
1153
+ user?: components["schemas"]["AuditUserRefResponse"] | null;
1154
+ /** Format: date-time */
1155
+ createdAt: string;
1156
+ /** Format: date-time */
1157
+ updatedAt: string;
1158
+ /** Format: date-time */
1159
+ deletedAt?: string | null;
1160
+ };
1161
+ PaginatedRequestLogsResponse: {
1162
+ items: components["schemas"]["RequestLogResponse"][];
1163
+ meta: components["schemas"]["PaginationMetaResponse"];
1164
+ };
1165
+ /** @enum {string} */
1166
+ ErrorType: "VALIDATION" | "AUTH" | "NOT_FOUND" | "RATE_LIMIT" | "DATABASE" | "INTEGRATION" | "INTERNAL";
1167
+ ErrorLogResponse: {
1168
+ /** Format: uuid */
1169
+ id: string;
1170
+ type: components["schemas"]["ErrorType"];
1171
+ /** @example 500 */
1172
+ statusCode?: number | null;
1173
+ message: string;
1174
+ stack?: string | null;
1175
+ /** @example POST */
1176
+ method?: string | null;
1177
+ path?: string | null;
1178
+ /** @example 10.0.0.1 */
1179
+ ipAddress?: string | null;
1180
+ /** Format: uuid */
1181
+ requestId?: string | null;
1182
+ userId?: string | null;
1183
+ user?: components["schemas"]["AuditUserRefResponse"] | null;
1184
+ /** Format: date-time */
1185
+ createdAt: string;
1186
+ /** Format: date-time */
1187
+ updatedAt: string;
1188
+ /** Format: date-time */
1189
+ deletedAt?: string | null;
1190
+ };
1191
+ PaginatedErrorLogsResponse: {
1192
+ items: components["schemas"]["ErrorLogResponse"][];
1193
+ meta: components["schemas"]["PaginationMetaResponse"];
1194
+ };
1195
+ /** @enum {string} */
1196
+ AuditDataAction: "CREATE" | "UPDATE" | "DELETE";
1035
1197
  AuditDataChangeResponse: {
1036
1198
  /** Format: uuid */
1037
1199
  id: string;
@@ -1696,7 +1858,9 @@ export interface operations {
1696
1858
  headers: {
1697
1859
  [name: string]: unknown;
1698
1860
  };
1699
- content?: never;
1861
+ content: {
1862
+ "application/json": components["schemas"]["ResourceResponse"][];
1863
+ };
1700
1864
  };
1701
1865
  };
1702
1866
  };
@@ -1713,11 +1877,13 @@ export interface operations {
1713
1877
  };
1714
1878
  };
1715
1879
  responses: {
1716
- 201: {
1880
+ 200: {
1717
1881
  headers: {
1718
1882
  [name: string]: unknown;
1719
1883
  };
1720
- content?: never;
1884
+ content: {
1885
+ "application/json": components["schemas"]["ResourceResponse"];
1886
+ };
1721
1887
  };
1722
1888
  };
1723
1889
  };
@@ -1759,7 +1925,9 @@ export interface operations {
1759
1925
  headers: {
1760
1926
  [name: string]: unknown;
1761
1927
  };
1762
- content?: never;
1928
+ content: {
1929
+ "application/json": components["schemas"]["ResourceResponse"];
1930
+ };
1763
1931
  };
1764
1932
  };
1765
1933
  };
@@ -1776,7 +1944,9 @@ export interface operations {
1776
1944
  headers: {
1777
1945
  [name: string]: unknown;
1778
1946
  };
1779
- content?: never;
1947
+ content: {
1948
+ "application/json": components["schemas"]["ActionResponse"][];
1949
+ };
1780
1950
  };
1781
1951
  };
1782
1952
  };
@@ -1793,11 +1963,13 @@ export interface operations {
1793
1963
  };
1794
1964
  };
1795
1965
  responses: {
1796
- 201: {
1966
+ 200: {
1797
1967
  headers: {
1798
1968
  [name: string]: unknown;
1799
1969
  };
1800
- content?: never;
1970
+ content: {
1971
+ "application/json": components["schemas"]["ActionResponse"];
1972
+ };
1801
1973
  };
1802
1974
  };
1803
1975
  };
@@ -1839,7 +2011,9 @@ export interface operations {
1839
2011
  headers: {
1840
2012
  [name: string]: unknown;
1841
2013
  };
1842
- content?: never;
2014
+ content: {
2015
+ "application/json": components["schemas"]["ActionResponse"];
2016
+ };
1843
2017
  };
1844
2018
  };
1845
2019
  };
@@ -1856,7 +2030,9 @@ export interface operations {
1856
2030
  headers: {
1857
2031
  [name: string]: unknown;
1858
2032
  };
1859
- content?: never;
2033
+ content: {
2034
+ "application/json": components["schemas"]["ScopeResponse"][];
2035
+ };
1860
2036
  };
1861
2037
  };
1862
2038
  };
@@ -1873,11 +2049,13 @@ export interface operations {
1873
2049
  };
1874
2050
  };
1875
2051
  responses: {
1876
- 201: {
2052
+ 200: {
1877
2053
  headers: {
1878
2054
  [name: string]: unknown;
1879
2055
  };
1880
- content?: never;
2056
+ content: {
2057
+ "application/json": components["schemas"]["ScopeResponse"];
2058
+ };
1881
2059
  };
1882
2060
  };
1883
2061
  };
@@ -1919,7 +2097,9 @@ export interface operations {
1919
2097
  headers: {
1920
2098
  [name: string]: unknown;
1921
2099
  };
1922
- content?: never;
2100
+ content: {
2101
+ "application/json": components["schemas"]["ScopeResponse"];
2102
+ };
1923
2103
  };
1924
2104
  };
1925
2105
  };
@@ -1936,7 +2116,9 @@ export interface operations {
1936
2116
  headers: {
1937
2117
  [name: string]: unknown;
1938
2118
  };
1939
- content?: never;
2119
+ content: {
2120
+ "application/json": components["schemas"]["PermissionResponse"][];
2121
+ };
1940
2122
  };
1941
2123
  };
1942
2124
  };
@@ -1953,11 +2135,13 @@ export interface operations {
1953
2135
  };
1954
2136
  };
1955
2137
  responses: {
1956
- 201: {
2138
+ 200: {
1957
2139
  headers: {
1958
2140
  [name: string]: unknown;
1959
2141
  };
1960
- content?: never;
2142
+ content: {
2143
+ "application/json": components["schemas"]["PermissionResponse"];
2144
+ };
1961
2145
  };
1962
2146
  };
1963
2147
  };
@@ -1999,7 +2183,9 @@ export interface operations {
1999
2183
  headers: {
2000
2184
  [name: string]: unknown;
2001
2185
  };
2002
- content?: never;
2186
+ content: {
2187
+ "application/json": components["schemas"]["PermissionResponse"];
2188
+ };
2003
2189
  };
2004
2190
  };
2005
2191
  };
@@ -2016,7 +2202,9 @@ export interface operations {
2016
2202
  headers: {
2017
2203
  [name: string]: unknown;
2018
2204
  };
2019
- content?: never;
2205
+ content: {
2206
+ "application/json": components["schemas"]["GroupResponse"][];
2207
+ };
2020
2208
  };
2021
2209
  };
2022
2210
  };
@@ -2033,11 +2221,13 @@ export interface operations {
2033
2221
  };
2034
2222
  };
2035
2223
  responses: {
2036
- 201: {
2224
+ 200: {
2037
2225
  headers: {
2038
2226
  [name: string]: unknown;
2039
2227
  };
2040
- content?: never;
2228
+ content: {
2229
+ "application/json": components["schemas"]["GroupResponse"];
2230
+ };
2041
2231
  };
2042
2232
  };
2043
2233
  };
@@ -2056,7 +2246,9 @@ export interface operations {
2056
2246
  headers: {
2057
2247
  [name: string]: unknown;
2058
2248
  };
2059
- content?: never;
2249
+ content: {
2250
+ "application/json": components["schemas"]["GroupResponse"];
2251
+ };
2060
2252
  };
2061
2253
  };
2062
2254
  };
@@ -2098,7 +2290,9 @@ export interface operations {
2098
2290
  headers: {
2099
2291
  [name: string]: unknown;
2100
2292
  };
2101
- content?: never;
2293
+ content: {
2294
+ "application/json": components["schemas"]["GroupResponse"];
2295
+ };
2102
2296
  };
2103
2297
  };
2104
2298
  };
@@ -2155,7 +2349,9 @@ export interface operations {
2155
2349
  headers: {
2156
2350
  [name: string]: unknown;
2157
2351
  };
2158
- content?: never;
2352
+ content: {
2353
+ "application/json": components["schemas"]["RoleResponse"][];
2354
+ };
2159
2355
  };
2160
2356
  };
2161
2357
  };
@@ -2172,11 +2368,13 @@ export interface operations {
2172
2368
  };
2173
2369
  };
2174
2370
  responses: {
2175
- 201: {
2371
+ 200: {
2176
2372
  headers: {
2177
2373
  [name: string]: unknown;
2178
2374
  };
2179
- content?: never;
2375
+ content: {
2376
+ "application/json": components["schemas"]["RoleResponse"];
2377
+ };
2180
2378
  };
2181
2379
  };
2182
2380
  };
@@ -2195,7 +2393,9 @@ export interface operations {
2195
2393
  headers: {
2196
2394
  [name: string]: unknown;
2197
2395
  };
2198
- content?: never;
2396
+ content: {
2397
+ "application/json": components["schemas"]["RoleResponse"];
2398
+ };
2199
2399
  };
2200
2400
  };
2201
2401
  };
@@ -2237,7 +2437,9 @@ export interface operations {
2237
2437
  headers: {
2238
2438
  [name: string]: unknown;
2239
2439
  };
2240
- content?: never;
2440
+ content: {
2441
+ "application/json": components["schemas"]["RoleResponse"];
2442
+ };
2241
2443
  };
2242
2444
  };
2243
2445
  };
@@ -2253,11 +2455,13 @@ export interface operations {
2253
2455
  };
2254
2456
  requestBody?: never;
2255
2457
  responses: {
2256
- 201: {
2458
+ 200: {
2257
2459
  headers: {
2258
2460
  [name: string]: unknown;
2259
2461
  };
2260
- content?: never;
2462
+ content: {
2463
+ "application/json": components["schemas"]["RoleResponse"];
2464
+ };
2261
2465
  };
2262
2466
  };
2263
2467
  };
@@ -2277,7 +2481,9 @@ export interface operations {
2277
2481
  headers: {
2278
2482
  [name: string]: unknown;
2279
2483
  };
2280
- content?: never;
2484
+ content: {
2485
+ "application/json": components["schemas"]["RoleResponse"];
2486
+ };
2281
2487
  };
2282
2488
  };
2283
2489
  };
@@ -2303,7 +2509,9 @@ export interface operations {
2303
2509
  headers: {
2304
2510
  [name: string]: unknown;
2305
2511
  };
2306
- content?: never;
2512
+ content: {
2513
+ "application/json": components["schemas"]["PaginatedRequestLogsResponse"];
2514
+ };
2307
2515
  };
2308
2516
  };
2309
2517
  };
@@ -2348,7 +2556,9 @@ export interface operations {
2348
2556
  headers: {
2349
2557
  [name: string]: unknown;
2350
2558
  };
2351
- content?: never;
2559
+ content: {
2560
+ "application/json": components["schemas"]["PaginatedErrorLogsResponse"];
2561
+ };
2352
2562
  };
2353
2563
  };
2354
2564
  };
@@ -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
- <Shield className="h-6 w-6 text-primary" />
224
+ {brand.mark}
225
225
  <span className="hidden text-lg font-extrabold sm:block">
226
- {t("app.name")}
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
- <Shield className="h-6 w-6 text-primary" />
293
- <span className="text-lg font-extrabold">{t("app.name")}</span>
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
+ }
@@ -1,5 +1,6 @@
1
1
  // Barrel da pasta — importe daqui de fora; dentro da pasta use o caminho direto.
2
2
  export * from "./app-shell";
3
+ export * from "./brand-header";
3
4
  export * from "./breadcrumbs";
4
5
  export * from "./language-selector";
5
6
  export * from "./password-field";
@@ -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
- {storageKey && hideable.length > 0 && (
231
- <Button
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
+ }
@@ -1,5 +1,6 @@
1
1
  // Barrel da pasta — importe daqui de fora; dentro da pasta use o caminho direto.
2
2
  export * from "./auth-context";
3
+ export * from "./brand-context";
3
4
  export * from "./color-mode-context";
4
5
  export * from "./i18n-context";
5
6
  export * from "./socket-context";
@@ -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
- <div className="mb-6 flex flex-col items-center">
43
- <div className="mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-primary text-primary-foreground">
44
- <Lock className="h-6 w-6" />
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
- <h1 className="mb-4 text-xl font-semibold">
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
- <h1 className="mb-4 text-xl font-semibold">{title}</h1>
95
+ <BrandHeader title={title} />
96
96
 
97
97
  {checking && (
98
98
  <div className="flex items-center gap-2 text-sm text-muted-foreground">