rl-core-front 0.18.0 → 0.18.1

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.18.0",
3
+ "version": "0.18.1",
4
4
  "description": "Telas e componentes Next.js do core: login com 2FA, usuários, RBAC, auditoria, logs e listagens com filtro dinâmico",
5
5
  "author": "Rodrigo Liberti",
6
6
  "license": "MIT",
@@ -9,6 +9,7 @@ import {
9
9
  GroupsPanel,
10
10
  RolesPanel,
11
11
  } from "#core/features/rbac/panels";
12
+ import { useTabState } from "#core/hooks/use-tab-state";
12
13
 
13
14
  /**
14
15
  * Controle de acesso, organizado pelo que se vem fazer aqui.
@@ -54,11 +55,16 @@ export function RbacScreen(): JSX.Element {
54
55
  },
55
56
  ].filter((tab) => tab.perm.some((code) => hasPermission(code)));
56
57
 
58
+ const { tab, setTab } = useTabState(
59
+ "rbac",
60
+ tabs.map((item) => item.value),
61
+ );
62
+
57
63
  return (
58
64
  <div>
59
65
  <h1 className="mb-4 text-3xl font-bold">{t("rbac.title")}</h1>
60
66
  {tabs.length > 0 && (
61
- <Tabs defaultValue={tabs[0].value}>
67
+ <Tabs value={tab} onValueChange={setTab}>
62
68
  <TabsList>
63
69
  {tabs.map((tab) => (
64
70
  <TabsTrigger key={tab.value} value={tab.value}>
@@ -0,0 +1,100 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+
5
+ import { safeStorage } from "#core/_utils/storage";
6
+
7
+ const STORAGE_PREFIX = "tab:";
8
+ const QUERY_PARAM = "tab";
9
+
10
+ export interface UseTabStateResult<T extends string> {
11
+ tab: T;
12
+ /**
13
+ * Recebe `string` porque é o que o `Tabs` e o `TabShortcuts` entregam; valor
14
+ * fora da lista é ignorado, para um link editado à mão não abrir aba nenhuma.
15
+ */
16
+ setTab: (next: string) => void;
17
+ }
18
+
19
+ const readFromUrl = (): string | null =>
20
+ new URLSearchParams(window.location.search).get(QUERY_PARAM);
21
+
22
+ /** Troca só o `?tab=`, preservando o resto — o `?filter=` da listagem mora ao lado. */
23
+ const writeToUrl = (value: string): void => {
24
+ const params = new URLSearchParams(window.location.search);
25
+ params.set(QUERY_PARAM, value);
26
+ window.history.replaceState(null, "", `${window.location.pathname}?${params.toString()}`);
27
+ };
28
+
29
+ /**
30
+ * Em qual aba a pessoa deixou a tela, por `storageKey`.
31
+ *
32
+ * Duas memórias, porque cada uma responde a uma pergunta diferente. A
33
+ * querystring (`?tab=`) faz o F5 voltar onde estava e transforma a aba em
34
+ * link compartilhável — o mesmo motivo do `?filter=` do `useFilters`. O
35
+ * `localStorage` cobre o que a URL não alcança: abrir a tela pelo menu amanhã
36
+ * e cair na última aba usada, e não na primeira.
37
+ *
38
+ * A URL manda quando as duas discordam: quem recebeu um link quer ver o que o
39
+ * link aponta, não a última aba de quem abriu.
40
+ *
41
+ * O espelho é `replaceState`, como no filtro: trocar de aba não é navegação, e
42
+ * empilhar histórico faria o "voltar" percorrer aba por aba antes de sair da
43
+ * tela.
44
+ *
45
+ * `tabs` é a lista do que existe: o que se lê da URL ou do storage e não está
46
+ * nela cai no padrão — uma aba que foi removida numa versão nova, ou um link
47
+ * digitado errado, abre a tela em vez de deixá-la vazia.
48
+ */
49
+ export function useTabState<T extends string>(
50
+ storageKey: string,
51
+ tabs: readonly T[],
52
+ defaultTab: T = tabs[0],
53
+ ): UseTabStateResult<T> {
54
+ const [tab, setStoredTab] = useState<T>(defaultTab);
55
+
56
+ // Por `ref` porque a lista costuma nascer dentro do render (filtrada por
57
+ // permissão, por exemplo): como dependência, ela refaria a hidratação a cada
58
+ // render.
59
+ const known = useRef(tabs);
60
+
61
+ useEffect(() => {
62
+ known.current = tabs;
63
+ });
64
+
65
+ const isTab = useCallback((value: string | null): value is T => {
66
+ return value !== null && (known.current as readonly string[]).includes(value);
67
+ }, []);
68
+
69
+ // Hidrata uma vez, no cliente: no primeiro render do Next não há `window`,
70
+ // e ler ali faria o servidor e o navegador desenharem abas diferentes.
71
+ useEffect(() => {
72
+ const fromUrl = readFromUrl();
73
+ const fromStorage = safeStorage.get(`${STORAGE_PREFIX}${storageKey}`);
74
+ const resolved = isTab(fromUrl) ? fromUrl : isTab(fromStorage) ? fromStorage : defaultTab;
75
+
76
+ setStoredTab(resolved);
77
+
78
+ // A URL passa a dizer onde a tela está, venha a aba de onde vier — sem
79
+ // isso, copiar o link depois de cair na aba do storage levaria à padrão.
80
+ // Lista vazia (nenhuma aba permitida) não tem o que dizer.
81
+ if (resolved !== undefined && fromUrl !== resolved) {
82
+ writeToUrl(resolved);
83
+ }
84
+ }, [storageKey, defaultTab, isTab]);
85
+
86
+ const setTab = useCallback(
87
+ (next: string): void => {
88
+ if (!isTab(next)) {
89
+ return;
90
+ }
91
+
92
+ setStoredTab(next);
93
+ writeToUrl(next);
94
+ safeStorage.set(`${STORAGE_PREFIX}${storageKey}`, next);
95
+ },
96
+ [storageKey, isTab],
97
+ );
98
+
99
+ return { tab, setTab };
100
+ }
package/src/index.ts CHANGED
@@ -121,6 +121,8 @@ export {
121
121
  TAB_SHORTCUT_DIGITS,
122
122
  useShortcut,
123
123
  } from "#core/hooks/use-shortcut";
124
+ export type { UseTabStateResult } from "#core/hooks/use-tab-state";
125
+ export { useTabState } from "#core/hooks/use-tab-state";
124
126
  export type { TableView, UseTableViewResult } from "#core/hooks/use-table-view";
125
127
  export { useTableView } from "#core/hooks/use-table-view";
126
128
  // RequestOperation é enum (valor, não só tipo): sem ele o projeto não consegue