@nsxbet/admin-sdk 0.5.1 → 0.6.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.
Files changed (173) hide show
  1. package/CHECKLIST.md +40 -10
  2. package/README.md +337 -36
  3. package/dist/auth/client/gateway-token.d.ts +19 -0
  4. package/dist/auth/client/gateway-token.js +89 -0
  5. package/dist/auth/client/in-memory.d.ts +5 -1
  6. package/dist/auth/client/in-memory.js +75 -38
  7. package/dist/auth/client/index.d.ts +0 -1
  8. package/dist/auth/client/interface.d.ts +6 -3
  9. package/dist/auth/client/keycloak.d.ts +0 -1
  10. package/dist/auth/client/keycloak.js +6 -3
  11. package/dist/auth/components/UserSelector.d.ts +0 -1
  12. package/dist/auth/components/UserSelector.js +89 -7
  13. package/dist/auth/components/index.d.ts +0 -1
  14. package/dist/auth/index.d.ts +0 -1
  15. package/dist/components/AuthProvider.d.ts +0 -1
  16. package/dist/components/Timestamp.d.ts +7 -0
  17. package/dist/components/Timestamp.js +50 -0
  18. package/dist/hooks/useAuth.d.ts +0 -1
  19. package/dist/hooks/useAuth.js +1 -1
  20. package/dist/hooks/useFetch.d.ts +0 -1
  21. package/dist/hooks/useI18n.d.ts +0 -1
  22. package/dist/hooks/usePlatformAPI.d.ts +0 -1
  23. package/dist/hooks/useTelemetry.d.ts +0 -1
  24. package/dist/hooks/useTimestamp.d.ts +8 -0
  25. package/dist/hooks/useTimestamp.js +122 -0
  26. package/dist/i18n/config.d.ts +20 -2
  27. package/dist/i18n/config.js +48 -0
  28. package/dist/i18n/index.d.ts +2 -3
  29. package/dist/i18n/index.js +1 -1
  30. package/dist/i18n/locales/en-US.json +95 -18
  31. package/dist/i18n/locales/es.json +95 -18
  32. package/dist/i18n/locales/pt-BR.json +95 -18
  33. package/dist/i18n/locales/ro.json +95 -18
  34. package/dist/index.d.ts +11 -7
  35. package/dist/index.js +5 -1
  36. package/dist/registry/AdminShellRegistry.d.ts +1 -2
  37. package/dist/registry/cache/cached-catalog.d.ts +11 -0
  38. package/dist/registry/cache/cached-catalog.js +42 -0
  39. package/dist/registry/cache/catalog-cache.d.ts +10 -0
  40. package/dist/registry/cache/catalog-cache.js +58 -0
  41. package/dist/registry/cache/index.d.ts +5 -0
  42. package/dist/registry/cache/index.js +3 -0
  43. package/dist/registry/cache/types.d.ts +20 -0
  44. package/dist/registry/cache/types.js +3 -0
  45. package/dist/registry/client/http.d.ts +0 -1
  46. package/dist/registry/client/http.js +13 -0
  47. package/dist/registry/client/in-memory.d.ts +0 -1
  48. package/dist/registry/client/in-memory.js +117 -12
  49. package/dist/registry/client/index.d.ts +0 -1
  50. package/dist/registry/client/interface.d.ts +21 -6
  51. package/dist/registry/index.d.ts +5 -2
  52. package/dist/registry/index.js +4 -0
  53. package/dist/registry/types/index.d.ts +2 -3
  54. package/dist/registry/types/manifest.d.ts +20 -24
  55. package/dist/registry/types/manifest.js +17 -18
  56. package/dist/registry/types/module.d.ts +43 -14
  57. package/dist/registry/useRegistryPolling.d.ts +15 -0
  58. package/dist/registry/useRegistryPolling.js +66 -0
  59. package/dist/router/DynamicModule.d.ts +6 -22
  60. package/dist/router/DynamicModule.js +25 -48
  61. package/dist/router/ModuleErrorBoundary.d.ts +39 -0
  62. package/dist/router/ModuleErrorBoundary.js +101 -0
  63. package/dist/router/index.d.ts +1 -1
  64. package/dist/router/url-allowlist.d.ts +22 -0
  65. package/dist/router/url-allowlist.js +65 -0
  66. package/dist/shell/AdminShell.d.ts +0 -1
  67. package/dist/shell/AdminShell.js +178 -43
  68. package/dist/shell/BackofficeShell.d.ts +0 -1
  69. package/dist/shell/BackofficeShell.js +59 -25
  70. package/dist/shell/components/CommandPalette.d.ts +0 -1
  71. package/dist/shell/components/CommandPalette.js +26 -50
  72. package/dist/shell/components/DevtoolsPanel.d.ts +11 -0
  73. package/dist/shell/components/DevtoolsPanel.js +145 -0
  74. package/dist/shell/components/HomePage.d.ts +0 -1
  75. package/dist/shell/components/HomePage.js +9 -4
  76. package/dist/shell/components/LeftNav.d.ts +0 -1
  77. package/dist/shell/components/LeftNav.js +91 -93
  78. package/dist/shell/components/MainContent.d.ts +3 -2
  79. package/dist/shell/components/MainContent.js +8 -23
  80. package/dist/shell/components/ModuleOverview.d.ts +0 -1
  81. package/dist/shell/components/ModuleOverview.js +4 -20
  82. package/dist/shell/components/ProfilePage.d.ts +0 -1
  83. package/dist/shell/components/ProfilePage.js +1 -1
  84. package/dist/shell/components/RegistryPage.d.ts +0 -1
  85. package/dist/shell/components/RegistryPage.js +154 -64
  86. package/dist/shell/components/RegistryStatusBanner.d.ts +6 -0
  87. package/dist/shell/components/RegistryStatusBanner.js +31 -0
  88. package/dist/shell/components/RegistryUnavailable.d.ts +4 -0
  89. package/dist/shell/components/RegistryUnavailable.js +7 -0
  90. package/dist/shell/components/SettingsPage.d.ts +0 -1
  91. package/dist/shell/components/StackedPanel.d.ts +15 -0
  92. package/dist/shell/components/StackedPanel.js +45 -0
  93. package/dist/shell/components/TopBar.d.ts +4 -2
  94. package/dist/shell/components/TopBar.js +9 -3
  95. package/dist/shell/components/UpdateBanner.d.ts +5 -0
  96. package/dist/shell/components/UpdateBanner.js +8 -0
  97. package/dist/shell/components/index.d.ts +4 -1
  98. package/dist/shell/components/index.js +2 -0
  99. package/dist/shell/components/theme-provider.d.ts +0 -1
  100. package/dist/shell/components/theme-provider.js +8 -5
  101. package/dist/shell/hooks/useCspViolations.d.ts +12 -0
  102. package/dist/shell/hooks/useCspViolations.js +34 -0
  103. package/dist/shell/index.d.ts +1 -2
  104. package/dist/shell/polling-config.d.ts +10 -0
  105. package/dist/shell/polling-config.js +26 -0
  106. package/dist/shell/search/fuzzy.d.ts +0 -1
  107. package/dist/shell/search/index.d.ts +0 -1
  108. package/dist/shell/telemetry.d.ts +0 -1
  109. package/dist/shell/types.d.ts +34 -18
  110. package/dist/tailwind/index.d.ts +0 -1
  111. package/dist/types/keycloak.d.ts +0 -1
  112. package/dist/types/platform.d.ts +12 -1
  113. package/dist/vite/AdminShellSharedDeps.d.ts +64 -0
  114. package/dist/vite/AdminShellSharedDeps.js +215 -0
  115. package/dist/vite/config.d.ts +2 -2
  116. package/dist/vite/config.js +5 -7
  117. package/dist/vite/i18n-plugin.d.ts +13 -0
  118. package/dist/vite/i18n-plugin.js +81 -0
  119. package/dist/vite/index.d.ts +2 -1
  120. package/dist/vite/index.js +2 -0
  121. package/dist/vite/plugins.d.ts +0 -1
  122. package/package.json +6 -2
  123. package/dist/auth/client/in-memory.d.ts.map +0 -1
  124. package/dist/auth/client/index.d.ts.map +0 -1
  125. package/dist/auth/client/interface.d.ts.map +0 -1
  126. package/dist/auth/client/keycloak.d.ts.map +0 -1
  127. package/dist/auth/components/UserSelector.d.ts.map +0 -1
  128. package/dist/auth/components/index.d.ts.map +0 -1
  129. package/dist/auth/index.d.ts.map +0 -1
  130. package/dist/components/AuthProvider.d.ts.map +0 -1
  131. package/dist/hooks/useAuth.d.ts.map +0 -1
  132. package/dist/hooks/useFetch.d.ts.map +0 -1
  133. package/dist/hooks/useI18n.d.ts.map +0 -1
  134. package/dist/hooks/usePlatformAPI.d.ts.map +0 -1
  135. package/dist/hooks/useTelemetry.d.ts.map +0 -1
  136. package/dist/i18n/config.d.ts.map +0 -1
  137. package/dist/i18n/index.d.ts.map +0 -1
  138. package/dist/index.d.ts.map +0 -1
  139. package/dist/registry/AdminShellRegistry.d.ts.map +0 -1
  140. package/dist/registry/client/http.d.ts.map +0 -1
  141. package/dist/registry/client/in-memory.d.ts.map +0 -1
  142. package/dist/registry/client/index.d.ts.map +0 -1
  143. package/dist/registry/client/interface.d.ts.map +0 -1
  144. package/dist/registry/index.d.ts.map +0 -1
  145. package/dist/registry/types/index.d.ts.map +0 -1
  146. package/dist/registry/types/manifest.d.ts.map +0 -1
  147. package/dist/registry/types/module.d.ts.map +0 -1
  148. package/dist/router/DynamicModule.d.ts.map +0 -1
  149. package/dist/router/index.d.ts.map +0 -1
  150. package/dist/shell/AdminShell.d.ts.map +0 -1
  151. package/dist/shell/BackofficeShell.d.ts.map +0 -1
  152. package/dist/shell/components/CommandPalette.d.ts.map +0 -1
  153. package/dist/shell/components/HomePage.d.ts.map +0 -1
  154. package/dist/shell/components/LeftNav.d.ts.map +0 -1
  155. package/dist/shell/components/MainContent.d.ts.map +0 -1
  156. package/dist/shell/components/ModuleOverview.d.ts.map +0 -1
  157. package/dist/shell/components/ProfilePage.d.ts.map +0 -1
  158. package/dist/shell/components/RegistryPage.d.ts.map +0 -1
  159. package/dist/shell/components/SettingsPage.d.ts.map +0 -1
  160. package/dist/shell/components/TopBar.d.ts.map +0 -1
  161. package/dist/shell/components/index.d.ts.map +0 -1
  162. package/dist/shell/components/theme-provider.d.ts.map +0 -1
  163. package/dist/shell/index.d.ts.map +0 -1
  164. package/dist/shell/search/fuzzy.d.ts.map +0 -1
  165. package/dist/shell/search/index.d.ts.map +0 -1
  166. package/dist/shell/telemetry.d.ts.map +0 -1
  167. package/dist/shell/types.d.ts.map +0 -1
  168. package/dist/tailwind/index.d.ts.map +0 -1
  169. package/dist/types/keycloak.d.ts.map +0 -1
  170. package/dist/types/platform.d.ts.map +0 -1
  171. package/dist/vite/config.d.ts.map +0 -1
  172. package/dist/vite/index.d.ts.map +0 -1
  173. package/dist/vite/plugins.d.ts.map +0 -1
@@ -1,17 +1,50 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState, useEffect, useCallback } from "react";
3
- import { Card, CardContent, CardDescription, CardHeader, CardTitle, Switch, Label, Button, Icon, Badge, Input, Textarea, Tabs, TabsList, TabsTrigger, TabsContent, } from "@nsxbet/admin-ui";
3
+ import { Card, CardContent, Switch, Label, Button, Icon, Badge, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, } from "@nsxbet/admin-ui";
4
+ import { useI18n } from "../../hooks/useI18n";
5
+ import { useAuth } from "../../hooks/useAuth";
6
+ import { resolveLocalizedString } from "../../i18n";
7
+ function formatRelativeDate(isoString) {
8
+ const now = Date.now();
9
+ const date = new Date(isoString).getTime();
10
+ const diffMs = now - date;
11
+ const diffMins = Math.floor(diffMs / 60000);
12
+ if (diffMins < 1)
13
+ return "just now";
14
+ if (diffMins < 60)
15
+ return `${diffMins}m ago`;
16
+ const diffHours = Math.floor(diffMins / 60);
17
+ if (diffHours < 24)
18
+ return `${diffHours}h ago`;
19
+ const diffDays = Math.floor(diffHours / 24);
20
+ if (diffDays < 30)
21
+ return `${diffDays}d ago`;
22
+ return new Date(isoString).toLocaleDateString();
23
+ }
4
24
  export function RegistryPage({ apiUrl, registryClient }) {
25
+ const { t, i18n } = useI18n();
26
+ const auth = useAuth();
5
27
  const [modules, setModules] = useState([]);
6
28
  const [isLoading, setIsLoading] = useState(true);
7
29
  const [error, setError] = useState(null);
8
- const [newModuleUrl, setNewModuleUrl] = useState("");
9
- const [manifestJson, setManifestJson] = useState("");
10
- const [isAdding, setIsAdding] = useState(false);
11
- // Fetch modules using registry client
30
+ const canView = auth.hasPermission("admin.platform.view");
31
+ const canEdit = auth.hasPermission("admin.platform.edit");
32
+ const canDelete = auth.hasPermission("admin.platform.delete");
33
+ const [disableTarget, setDisableTarget] = useState(null);
34
+ const [historyTarget, setHistoryTarget] = useState(null);
35
+ const [versionHistory, setVersionHistory] = useState([]);
36
+ const [isLoadingHistory, setIsLoadingHistory] = useState(false);
37
+ const [rollbackTarget, setRollbackTarget] = useState(null);
38
+ const [isRollingBack, setIsRollingBack] = useState(false);
39
+ const [isReorderMode, setIsReorderMode] = useState(false);
40
+ const [reorderList, setReorderList] = useState([]);
41
+ const [originalOrder, setOriginalOrder] = useState([]);
42
+ const [isSavingOrder, setIsSavingOrder] = useState(false);
43
+ const hasOrderChanged = isReorderMode &&
44
+ reorderList.some((m, i) => m.id !== originalOrder[i]);
12
45
  const fetchModules = useCallback(async () => {
13
46
  if (!registryClient) {
14
- setError("Registry client not available.");
47
+ setError(t("registryPage.errorClientUnavailable"));
15
48
  setIsLoading(false);
16
49
  return;
17
50
  }
@@ -22,108 +55,165 @@ export function RegistryPage({ apiUrl, registryClient }) {
22
55
  setModules(data);
23
56
  }
24
57
  catch (err) {
25
- setError(err instanceof Error ? err.message : "Failed to load modules");
58
+ setError(err instanceof Error ? err.message : t("registryPage.errorLoadModules"));
26
59
  }
27
60
  finally {
28
61
  setIsLoading(false);
29
62
  }
30
- }, [registryClient]);
63
+ }, [registryClient, t]);
31
64
  useEffect(() => {
32
65
  fetchModules();
33
66
  }, [fetchModules]);
34
- // Add module by URL
35
- const handleAddModuleByUrl = async () => {
36
- if (!newModuleUrl.trim() || !registryClient) {
67
+ const handleToggleEnabled = async (module) => {
68
+ if (!registryClient)
69
+ return;
70
+ if (module.enabled) {
71
+ setDisableTarget(module);
37
72
  return;
38
73
  }
39
74
  try {
40
- setIsAdding(true);
41
75
  setError(null);
42
- await registryClient.modules.register(newModuleUrl.trim());
43
- setNewModuleUrl("");
76
+ await registryClient.modules.update(module.id, { enabled: true });
44
77
  await fetchModules();
45
78
  }
46
79
  catch (err) {
47
- setError(err instanceof Error ? err.message : "Failed to add module");
80
+ setError(err instanceof Error ? err.message : t("registryPage.errorUpdateModule"));
48
81
  }
49
- finally {
50
- setIsAdding(false);
82
+ };
83
+ const confirmDisable = async () => {
84
+ if (!registryClient || !disableTarget)
85
+ return;
86
+ try {
87
+ setError(null);
88
+ await registryClient.modules.update(disableTarget.id, { enabled: false });
89
+ setDisableTarget(null);
90
+ await fetchModules();
91
+ }
92
+ catch (err) {
93
+ setError(err instanceof Error ? err.message : t("registryPage.errorDisableModule"));
51
94
  }
52
95
  };
53
- // Add module by manifest JSON
54
- const handleAddModuleByManifest = async () => {
55
- if (!manifestJson.trim() || !registryClient)
96
+ const handleDeleteModule = async (module) => {
97
+ if (!registryClient)
98
+ return;
99
+ const name = resolveLocalizedString(module.manifest.title, i18n.language);
100
+ const confirmed = window.confirm(t("registryPage.deleteConfirm", { name }));
101
+ if (!confirmed)
56
102
  return;
57
103
  try {
58
- setIsAdding(true);
59
104
  setError(null);
60
- // Parse the manifest JSON
61
- let manifest;
62
- try {
63
- manifest = JSON.parse(manifestJson.trim());
64
- }
65
- catch {
66
- throw new Error("Invalid JSON format. Please check your manifest.");
67
- }
68
- await registryClient.modules.registerFromManifest(manifest, "");
69
- setManifestJson("");
105
+ await registryClient.modules.delete(module.id);
70
106
  await fetchModules();
71
107
  }
72
108
  catch (err) {
73
- setError(err instanceof Error ? err.message : "Failed to add module");
109
+ setError(err instanceof Error ? err.message : t("registryPage.errorDeleteModule"));
110
+ }
111
+ };
112
+ const openVersionHistory = async (module) => {
113
+ if (!registryClient)
114
+ return;
115
+ setHistoryTarget(module);
116
+ setIsLoadingHistory(true);
117
+ setVersionHistory([]);
118
+ try {
119
+ const versions = await registryClient.modules.listVersions(module.id);
120
+ setVersionHistory(versions);
121
+ }
122
+ catch (err) {
123
+ setError(err instanceof Error ? err.message : t("registryPage.errorLoadVersions"));
124
+ setHistoryTarget(null);
74
125
  }
75
126
  finally {
76
- setIsAdding(false);
127
+ setIsLoadingHistory(false);
77
128
  }
78
129
  };
79
- // Toggle module enabled state
80
- const handleToggleEnabled = async (module) => {
81
- if (!registryClient)
130
+ const confirmRollback = async () => {
131
+ if (!registryClient || !rollbackTarget)
82
132
  return;
83
133
  try {
134
+ setIsRollingBack(true);
84
135
  setError(null);
85
- await registryClient.modules.update(module.id, {
86
- enabled: !module.enabled,
87
- });
136
+ await registryClient.modules.rollback(rollbackTarget.module.id, rollbackTarget.version.version);
137
+ setRollbackTarget(null);
88
138
  await fetchModules();
139
+ if (historyTarget) {
140
+ const versions = await registryClient.modules.listVersions(historyTarget.id);
141
+ setVersionHistory(versions);
142
+ }
89
143
  }
90
144
  catch (err) {
91
- setError(err instanceof Error ? err.message : "Failed to update module");
145
+ setError(err instanceof Error ? err.message : t("registryPage.errorRollback"));
146
+ }
147
+ finally {
148
+ setIsRollingBack(false);
92
149
  }
93
150
  };
94
- // Delete module
95
- const handleDeleteModule = async (module) => {
96
- if (!registryClient)
151
+ const enterReorderMode = () => {
152
+ setReorderList([...modules]);
153
+ setOriginalOrder(modules.map((m) => m.id));
154
+ setIsReorderMode(true);
155
+ };
156
+ const cancelReorder = () => {
157
+ setIsReorderMode(false);
158
+ setReorderList([]);
159
+ };
160
+ const moveUp = (index) => {
161
+ if (index <= 0)
97
162
  return;
98
- const confirmed = window.confirm(`Are you sure you want to delete "${module.manifest.title}"? This action cannot be undone.`);
99
- if (!confirmed)
163
+ const updated = [...reorderList];
164
+ [updated[index - 1], updated[index]] = [updated[index], updated[index - 1]];
165
+ setReorderList(updated);
166
+ };
167
+ const moveDown = (index) => {
168
+ if (index >= reorderList.length - 1)
169
+ return;
170
+ const updated = [...reorderList];
171
+ [updated[index], updated[index + 1]] = [updated[index + 1], updated[index]];
172
+ setReorderList(updated);
173
+ };
174
+ const saveOrder = async () => {
175
+ if (!registryClient)
100
176
  return;
101
177
  try {
178
+ setIsSavingOrder(true);
102
179
  setError(null);
103
- await registryClient.modules.delete(module.id);
180
+ const order = reorderList.map((m, i) => ({
181
+ id: m.id,
182
+ navigationOrder: (i + 1) * 10,
183
+ }));
184
+ await registryClient.modules.reorder(order);
185
+ setIsReorderMode(false);
186
+ setReorderList([]);
104
187
  await fetchModules();
105
188
  }
106
189
  catch (err) {
107
- setError(err instanceof Error ? err.message : "Failed to delete module");
190
+ setError(err instanceof Error ? err.message : t("registryPage.errorSaveOrder"));
191
+ }
192
+ finally {
193
+ setIsSavingOrder(false);
108
194
  }
109
195
  };
196
+ if (!canView) {
197
+ return (_jsxs("div", { className: "p-6 max-w-4xl mx-auto space-y-6", "data-testid": "registry-page", children: [_jsx("div", { children: _jsx("h1", { className: "text-3xl font-bold mb-1", "data-testid": "registry-heading", children: t("registryPage.title") }) }), _jsx(Card, { className: "border-destructive", children: _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "flex items-center gap-3 text-destructive", children: [_jsx(Icon, { name: "shield-x", className: "h-5 w-5" }), _jsx("p", { children: t("errors.accessDeniedDescription") })] }) }) })] }));
198
+ }
110
199
  if (!registryClient) {
111
- return (_jsxs("div", { className: "p-6 max-w-4xl mx-auto space-y-6", "data-testid": "registry-page", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-bold mb-1", "data-testid": "registry-heading", children: "Module Registry" }), _jsx("p", { className: "text-muted-foreground", children: "Manage registered modules" })] }), _jsx(Card, { className: "border-destructive", children: _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "flex items-center gap-3 text-destructive", children: [_jsx(Icon, { name: "alert-circle", className: "h-5 w-5" }), _jsx("p", { children: "Registry client not available. Please check configuration." })] }) }) })] }));
200
+ return (_jsxs("div", { className: "p-6 max-w-4xl mx-auto space-y-6", "data-testid": "registry-page", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-bold mb-1", "data-testid": "registry-heading", children: t("registryPage.title") }), _jsx("p", { className: "text-muted-foreground", children: t("registryPage.description") })] }), _jsx(Card, { className: "border-destructive", children: _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "flex items-center gap-3 text-destructive", children: [_jsx(Icon, { name: "alert-circle", className: "h-5 w-5" }), _jsx("p", { children: t("registryPage.errorClientUnavailable") })] }) }) })] }));
112
201
  }
113
- return (_jsxs("div", { className: "p-6 max-w-4xl mx-auto space-y-6", "data-testid": "registry-page", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-bold mb-1", "data-testid": "registry-heading", children: "Module Registry" }), _jsx("p", { className: "text-muted-foreground", children: "Manage registered modules" })] }), _jsx(Badge, { variant: apiUrl ? "default" : "secondary", children: apiUrl ? "API Mode" : "In-Memory Mode" })] }), error && (_jsx(Card, { className: "border-destructive bg-destructive/10", children: _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "flex items-center gap-3 text-destructive", children: [_jsx(Icon, { name: "alert-circle", className: "h-5 w-5" }), _jsx("p", { children: error })] }) }) })), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "plus", className: "h-5 w-5 text-muted-foreground" }), _jsx(CardTitle, { className: "text-lg", children: "Add Module" })] }), _jsx(CardDescription, { children: "Register a new module by URL or manifest" })] }), _jsx(CardContent, { children: _jsxs(Tabs, { defaultValue: "url", className: "w-full", children: [_jsxs(TabsList, { className: "grid w-full grid-cols-2 mb-4", children: [_jsxs(TabsTrigger, { value: "url", children: [_jsx(Icon, { name: "link", className: "h-4 w-4 mr-2" }), "From URL"] }), _jsxs(TabsTrigger, { value: "manifest", children: [_jsx(Icon, { name: "file-text", className: "h-4 w-4 mr-2" }), "From Manifest"] })] }), _jsx(TabsContent, { value: "url", children: _jsxs("form", { onSubmit: (e) => {
114
- e.preventDefault();
115
- handleAddModuleByUrl();
116
- }, className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "module-url", children: "Module Base URL" }), _jsx(Input, { id: "module-url", type: "text", placeholder: "http://localhost:3003/dist", value: newModuleUrl, onChange: (e) => setNewModuleUrl(e.target.value), disabled: isAdding }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Enter the base URL where the module is served. The system will fetch admin.module.json from this URL." })] }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { type: "submit", disabled: !newModuleUrl.trim() || isAdding, children: isAdding ? (_jsxs(_Fragment, { children: [_jsx(Icon, { name: "loader-2", className: "h-4 w-4 mr-2 animate-spin" }), "Adding..."] })) : (_jsxs(_Fragment, { children: [_jsx(Icon, { name: "plus", className: "h-4 w-4 mr-2" }), "Add Module"] })) }) })] }) }), _jsx(TabsContent, { value: "manifest", children: _jsxs("form", { onSubmit: (e) => {
117
- e.preventDefault();
118
- handleAddModuleByManifest();
119
- }, className: "space-y-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "module-manifest", children: "Module Manifest (JSON)" }), _jsx(Textarea, { id: "module-manifest", placeholder: `{
120
- "id": "@admin/my-module",
121
- "title": "My Module",
122
- "description": "Module description",
123
- "category": "Tools",
124
- "icon": "package",
125
- "routeBase": "/admin/my-module",
126
- "keywords": ["keyword1", "keyword2"],
127
- "status": "active"
128
- }`, value: manifestJson, onChange: (e) => setManifestJson(e.target.value), disabled: isAdding, rows: 10, className: "font-mono text-sm" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Paste the module manifest JSON. Required fields: id, title, routeBase." })] }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { type: "submit", disabled: !manifestJson.trim() || isAdding, children: isAdding ? (_jsxs(_Fragment, { children: [_jsx(Icon, { name: "loader-2", className: "h-4 w-4 mr-2 animate-spin" }), "Adding..."] })) : (_jsxs(_Fragment, { children: [_jsx(Icon, { name: "plus", className: "h-4 w-4 mr-2" }), "Add Module"] })) }) })] }) })] }) })] }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "package", className: "h-5 w-5 text-muted-foreground" }), _jsxs("h2", { className: "text-lg font-semibold", children: ["Registered Modules", " ", !isLoading && (_jsxs("span", { className: "text-muted-foreground font-normal", children: ["(", modules.length, ")"] }))] })] }), isLoading ? (_jsx(Card, { children: _jsxs(CardContent, { className: "py-8 text-center", children: [_jsx(Icon, { name: "loader-2", className: "h-8 w-8 mx-auto mb-3 animate-spin text-muted-foreground" }), _jsx("p", { className: "text-muted-foreground", children: "Loading modules..." })] }) })) : modules.length === 0 ? (_jsx(Card, { children: _jsxs(CardContent, { className: "py-8 text-center", children: [_jsx(Icon, { name: "package", className: "h-8 w-8 mx-auto mb-3 text-muted-foreground" }), _jsx("p", { className: "text-muted-foreground", children: "No modules registered yet." }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Add a module using the form above." })] }) })) : (_jsx("div", { className: "space-y-3", children: modules.map((module) => (_jsx(Card, { className: !module.enabled ? "opacity-60 border-dashed" : undefined, children: _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "flex items-start gap-4 flex-1 min-w-0", children: [_jsx("div", { className: "flex-shrink-0 p-2 rounded-lg bg-muted", children: _jsx(Icon, { name: module.manifest.icon || "package", className: "h-6 w-6" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2 mb-1", children: [_jsx("h3", { className: "font-semibold truncate", children: module.manifest.title }), _jsx(Badge, { variant: module.enabled ? "success" : "secondary", children: module.enabled ? "Enabled" : "Disabled" })] }), _jsx("p", { className: "text-sm text-muted-foreground mb-3", children: module.manifest.description || "No description" }), _jsxs("div", { className: "flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground", children: [_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Icon, { name: "hash", className: "h-3 w-3" }), module.moduleId] }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Icon, { name: "folder", className: "h-3 w-3" }), module.manifest.category || "General"] }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Icon, { name: "navigation", className: "h-3 w-3" }), module.manifest.routeBase] }), module.baseUrl && (_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Icon, { name: "link", className: "h-3 w-3" }), module.baseUrl] }))] })] })] }), _jsxs("div", { className: "flex items-center gap-3 flex-shrink-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { className: "text-sm text-muted-foreground", children: "Enabled" }), _jsx(Switch, { checked: module.enabled, onCheckedChange: () => handleToggleEnabled(module) })] }), _jsx(Button, { variant: "destructive", size: "sm", onClick: () => handleDeleteModule(module), children: _jsx(Icon, { name: "trash-2", className: "h-4 w-4" }) })] })] }) }) }, module.id))) }))] })] }));
202
+ return (_jsxs("div", { className: "p-6 max-w-4xl mx-auto space-y-6", "data-testid": "registry-page", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-bold mb-1", "data-testid": "registry-heading", children: t("registryPage.title") }), _jsx("p", { className: "text-muted-foreground", children: t("registryPage.description") })] }), _jsxs("div", { className: "flex items-center gap-2", children: [canEdit && !isReorderMode && (_jsxs(Button, { variant: "outline", size: "sm", onClick: enterReorderMode, children: [_jsx(Icon, { name: "arrow-up-down", className: "h-4 w-4 mr-2" }), t("registryPage.manageOrder")] })), _jsx(Badge, { variant: apiUrl ? "default" : "secondary", children: apiUrl ? t("registryPage.apiMode") : t("registryPage.inMemoryMode") })] })] }), error && (_jsx(Card, { className: "border-destructive bg-destructive/10", children: _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "flex items-center gap-3 text-destructive", children: [_jsx(Icon, { name: "alert-circle", className: "h-5 w-5" }), _jsx("p", { children: error })] }) }) })), isReorderMode && (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6", children: [_jsxs("div", { className: "flex items-center justify-between mb-4", children: [_jsx("h2", { className: "text-lg font-semibold", children: t("registryPage.manageOrder") }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: cancelReorder, disabled: isSavingOrder, children: t("common.cancel") }), _jsx(Button, { size: "sm", onClick: saveOrder, disabled: isSavingOrder || !hasOrderChanged, children: isSavingOrder ? (_jsxs(_Fragment, { children: [_jsx(Icon, { name: "loader-2", className: "h-4 w-4 mr-2 animate-spin" }), t("registryPage.savingOrder")] })) : (t("registryPage.saveOrder")) })] })] }), _jsx("div", { className: "space-y-1", children: reorderList.map((module, index) => (_jsxs("div", { className: "flex items-center gap-3 p-2 rounded-lg border bg-background", children: [_jsx(Badge, { variant: "outline", className: "font-mono text-xs shrink-0", children: t("registryPage.position", { position: index + 1 }) }), _jsx("div", { className: "flex-shrink-0 p-1.5 rounded bg-muted", children: _jsx(Icon, { name: module.manifest.icon || "package", className: "h-4 w-4" }) }), _jsx("span", { className: "flex-1 font-medium truncate", children: resolveLocalizedString(module.manifest.title, i18n.language) }), _jsxs("div", { className: "flex gap-1 shrink-0", children: [_jsx(Button, { variant: "ghost", size: "icon", className: "h-7 w-7", onClick: () => moveUp(index), disabled: index === 0, children: _jsx(Icon, { name: "chevron-up", className: "h-4 w-4" }) }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-7 w-7", onClick: () => moveDown(index), disabled: index === reorderList.length - 1, children: _jsx(Icon, { name: "chevron-down", className: "h-4 w-4" }) })] })] }, module.id))) })] }) })), !isReorderMode && (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "package", className: "h-5 w-5 text-muted-foreground" }), _jsxs("h2", { className: "text-lg font-semibold", children: [t("registryPage.registeredModules"), " ", !isLoading && (_jsxs("span", { className: "text-muted-foreground font-normal", children: ["(", modules.length, ")"] }))] })] }), isLoading ? (_jsx(Card, { children: _jsxs(CardContent, { className: "py-8 text-center", children: [_jsx(Icon, { name: "loader-2", className: "h-8 w-8 mx-auto mb-3 animate-spin text-muted-foreground" }), _jsx("p", { className: "text-muted-foreground", children: t("registryPage.loadingModules") })] }) })) : modules.length === 0 ? (_jsx(Card, { children: _jsxs(CardContent, { className: "py-8 text-center", children: [_jsx(Icon, { name: "package", className: "h-8 w-8 mx-auto mb-3 text-muted-foreground" }), _jsx("p", { className: "text-muted-foreground", children: t("registryPage.noModules") }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: t("registryPage.noModulesHint") })] }) })) : (_jsx("div", { className: "space-y-3", children: modules.map((module, index) => (_jsx(Card, { className: !module.enabled ? "opacity-60 border-dashed" : undefined, "data-testid": `module-card-${module.moduleId}`, children: _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "flex items-start gap-4 flex-1 min-w-0", children: [_jsx("div", { className: "flex-shrink-0 p-2 rounded-lg bg-muted", children: _jsx(Icon, { name: module.manifest.icon || "package", className: "h-6 w-6" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2 mb-1 flex-wrap", children: [_jsx("h3", { className: "font-semibold truncate", children: resolveLocalizedString(module.manifest.title, i18n.language) }), canEdit && (_jsx(Badge, { variant: module.enabled ? "success" : "secondary", children: module.enabled
203
+ ? t("registryPage.enabled")
204
+ : t("registryPage.disabled") })), module.version ? (_jsxs(Badge, { variant: "outline", "data-testid": `version-badge-${module.moduleId}`, children: ["v", module.version] })) : (_jsx(Badge, { variant: "outline", className: "text-muted-foreground", children: t("registryPage.unversioned") }))] }), _jsx("p", { className: "text-sm text-muted-foreground mb-3", children: resolveLocalizedString(module.manifest.description, i18n.language) ||
205
+ t("registryPage.noDescription") }), _jsxs("div", { className: "flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground", children: [_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Icon, { name: "hash", className: "h-3 w-3" }), module.moduleId] }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Icon, { name: "folder", className: "h-3 w-3" }), module.manifest.category || "General"] }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Icon, { name: "navigation", className: "h-3 w-3" }), module.manifest.routeBase] }), module.baseUrl && (_jsxs("span", { className: "flex items-center gap-1", title: module.baseUrl, children: [_jsx(Icon, { name: "link", className: "h-3 w-3" }), _jsx("span", { className: "truncate max-w-[200px]", children: module.baseUrl })] }))] })] })] }), _jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [_jsx(Badge, { variant: "outline", className: "font-mono text-xs", children: t("registryPage.position", { position: index + 1 }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => openVersionHistory(module), title: t("registryPage.versionHistory"), "data-testid": `version-history-btn-${module.moduleId}`, children: _jsx(Icon, { name: "history", className: "h-4 w-4" }) }), canEdit && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { className: "text-sm text-muted-foreground", children: t("registryPage.enabled") }), _jsx(Switch, { checked: module.enabled, onCheckedChange: () => handleToggleEnabled(module), "data-testid": `enable-switch-${module.moduleId}` })] })), canDelete && (_jsx(Button, { variant: "destructive", size: "sm", onClick: () => handleDeleteModule(module), children: _jsx(Icon, { name: "trash-2", className: "h-4 w-4" }) }))] })] }) }) }, module.id))) }))] })), _jsx(Dialog, { open: !!disableTarget, onOpenChange: (open) => !open && setDisableTarget(null), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t("registryPage.disableTitle", {
206
+ name: disableTarget
207
+ ? resolveLocalizedString(disableTarget.manifest.title, i18n.language)
208
+ : "",
209
+ }) }), _jsx(DialogDescription, { children: t("registryPage.disableDescription") })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setDisableTarget(null), children: t("common.cancel") }), _jsx(Button, { variant: "destructive", onClick: confirmDisable, children: t("registryPage.disableConfirm") })] })] }) }), _jsx(Sheet, { open: !!historyTarget, onOpenChange: (open) => !open && setHistoryTarget(null), children: _jsxs(SheetContent, { className: "sm:max-w-md", children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: t("registryPage.versionHistory") }), _jsx(SheetDescription, { children: historyTarget && resolveLocalizedString(historyTarget.manifest.title, i18n.language) })] }), _jsx("div", { className: "mt-6 space-y-3", "data-testid": "version-history-list", children: isLoadingHistory ? (_jsxs("div", { className: "py-8 text-center", children: [_jsx(Icon, { name: "loader-2", className: "h-6 w-6 mx-auto mb-2 animate-spin text-muted-foreground" }), _jsx("p", { className: "text-sm text-muted-foreground", children: t("registryPage.loadingVersions") })] })) : versionHistory.length === 0 ? (_jsxs("div", { className: "py-8 text-center", children: [_jsx(Icon, { name: "package", className: "h-6 w-6 mx-auto mb-2 text-muted-foreground" }), _jsx("p", { className: "text-sm text-muted-foreground", children: t("registryPage.noVersionHistory") })] })) : (versionHistory.map((v) => (_jsxs("div", { className: "flex items-center justify-between gap-3 p-3 rounded-lg border", "data-testid": `version-entry-${v.version}`, children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2 mb-0.5", children: [_jsxs("span", { className: "font-mono text-sm font-medium", children: ["v", v.version] }), v.active && (_jsx(Badge, { variant: "success", className: "text-xs", children: t("registryPage.versionActive") }))] }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [formatRelativeDate(v.publishedAt), typeof v.metadata?.rolledBackFrom === "string" && (_jsx("span", { className: "ml-2 text-amber-600", children: t("registryPage.rolledBackFrom", {
210
+ version: v.metadata.rolledBackFrom,
211
+ }) }))] })] }), !v.active && historyTarget && canEdit && (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setRollbackTarget({ module: historyTarget, version: v }), "data-testid": `rollback-btn-${v.version}`, children: [_jsx(Icon, { name: "undo-2", className: "h-3.5 w-3.5 mr-1.5" }), t("registryPage.rollback")] }))] }, v.id)))) })] }) }), _jsx(Dialog, { open: !!rollbackTarget, onOpenChange: (open) => !open && setRollbackTarget(null), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t("registryPage.rollbackTitle", {
212
+ version: rollbackTarget?.version.version ?? "",
213
+ }) }), _jsx(DialogDescription, { children: rollbackTarget &&
214
+ t("registryPage.rollbackDescription", {
215
+ name: resolveLocalizedString(rollbackTarget.module.manifest.title, i18n.language),
216
+ fromVersion: rollbackTarget.module.version ?? "",
217
+ toVersion: rollbackTarget.version.version,
218
+ }) })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setRollbackTarget(null), disabled: isRollingBack, children: t("common.cancel") }), _jsx(Button, { onClick: confirmRollback, disabled: isRollingBack, children: isRollingBack ? (_jsxs(_Fragment, { children: [_jsx(Icon, { name: "loader-2", className: "h-4 w-4 mr-2 animate-spin" }), t("registryPage.rollingBack")] })) : (_jsxs(_Fragment, { children: [_jsx(Icon, { name: "undo-2", className: "h-4 w-4 mr-2" }), t("registryPage.rollbackConfirm")] })) })] })] }) })] }));
129
219
  }
@@ -0,0 +1,6 @@
1
+ import type { RegistryCacheStatus } from "../../registry/cache/types";
2
+ export interface RegistryStatusBannerProps {
3
+ status: RegistryCacheStatus;
4
+ onRetry: () => void;
5
+ }
6
+ export declare function RegistryStatusBanner({ status, onRetry }: RegistryStatusBannerProps): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,31 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Alert, AlertDescription } from "@nsxbet/admin-ui";
3
+ import { useI18n } from "../../hooks/useI18n";
4
+ function formatRelativeTime(isoTimestamp) {
5
+ const diff = Date.now() - new Date(isoTimestamp).getTime();
6
+ const minutes = Math.floor(diff / 60000);
7
+ if (minutes < 1)
8
+ return "just now";
9
+ if (minutes < 60)
10
+ return `${minutes}m ago`;
11
+ const hours = Math.floor(minutes / 60);
12
+ if (hours < 24)
13
+ return `${hours}h ago`;
14
+ const days = Math.floor(hours / 24);
15
+ return `${days}d ago`;
16
+ }
17
+ export function RegistryStatusBanner({ status, onRetry }) {
18
+ const { t } = useI18n();
19
+ if (status.state === "fresh" || status.state === "unavailable") {
20
+ return null;
21
+ }
22
+ const isCached = status.state === "cached";
23
+ const className = isCached
24
+ ? "border-blue-200 bg-blue-50 text-blue-800 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-200"
25
+ : "border-amber-200 bg-amber-50 text-amber-800 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-200";
26
+ const message = isCached
27
+ ? t("registryCache.cachedMessage")
28
+ : t("registryCache.staleMessage");
29
+ const relativeTime = formatRelativeTime(status.cachedAt);
30
+ return (_jsx(Alert, { variant: "default", className: className, "data-testid": "registry-status-banner", children: _jsxs(AlertDescription, { className: "flex items-center justify-between", children: [_jsxs("span", { "data-testid": "registry-banner-message", children: [message, " ", _jsxs("span", { className: "opacity-75", children: ["(", t("registryCache.lastUpdated", { time: relativeTime }), ")"] })] }), _jsx("button", { onClick: onRetry, className: "ml-4 shrink-0 underline underline-offset-2 hover:opacity-80", "data-testid": "registry-banner-retry", children: t("registryCache.retry") })] }) }));
31
+ }
@@ -0,0 +1,4 @@
1
+ export interface RegistryUnavailableProps {
2
+ onRetry: () => void;
3
+ }
4
+ export declare function RegistryUnavailable({ onRetry }: RegistryUnavailableProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button } from "@nsxbet/admin-ui";
3
+ import { useI18n } from "../../hooks/useI18n";
4
+ export function RegistryUnavailable({ onRetry }) {
5
+ const { t } = useI18n();
6
+ return (_jsxs("div", { className: "flex h-screen flex-col items-center justify-center gap-4", "data-testid": "registry-unavailable", children: [_jsx("h1", { className: "text-2xl font-semibold text-foreground", children: t("registryCache.unavailableTitle") }), _jsx("p", { className: "max-w-md text-center text-muted-foreground", children: t("registryCache.unavailableDescription") }), _jsx(Button, { onClick: onRetry, "data-testid": "registry-unavailable-retry", children: t("registryCache.retry") })] }));
7
+ }
@@ -1,2 +1 @@
1
1
  export declare function SettingsPage(): import("react/jsx-runtime").JSX.Element;
2
- //# sourceMappingURL=SettingsPage.d.ts.map
@@ -0,0 +1,15 @@
1
+ import type { Module } from "../types";
2
+ import type { LocalizedField } from "../../i18n/config.js";
3
+ interface StackedPanelProps {
4
+ module: Module;
5
+ showPinned: boolean;
6
+ isCommandPinned: (moduleId: string, commandId: string) => boolean;
7
+ togglePin: (module: Module, command: {
8
+ id: string;
9
+ title: LocalizedField;
10
+ route: string;
11
+ icon?: string;
12
+ }) => void;
13
+ }
14
+ export declare function StackedPanel({ module, showPinned, isCommandPinned, togglePin, }: StackedPanelProps): import("react/jsx-runtime").JSX.Element;
15
+ export {};
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useNavigate, useLocation } from "react-router-dom";
3
+ import { useI18n } from "../../hooks/useI18n";
4
+ import { resolveLocalizedString } from "../../i18n";
5
+ import { SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton, useSidebar, Icon, ChevronLeft, Pin, } from "@nsxbet/admin-ui";
6
+ export function StackedPanel({ module, showPinned, isCommandPinned, togglePin, }) {
7
+ const navigate = useNavigate();
8
+ const location = useLocation();
9
+ const { i18n } = useI18n();
10
+ const { state } = useSidebar();
11
+ const isCollapsed = state === "collapsed";
12
+ const moduleTitle = resolveLocalizedString(module.title, i18n.language);
13
+ const sections = module.navigation?.sections ?? [];
14
+ const commands = module.commands ?? [];
15
+ const isRouteActive = (route) => {
16
+ return location.pathname === route || location.pathname.startsWith(route + "/");
17
+ };
18
+ const commandsBySection = new Map();
19
+ for (const cmd of commands) {
20
+ const key = cmd.section;
21
+ if (!commandsBySection.has(key)) {
22
+ commandsBySection.set(key, []);
23
+ }
24
+ commandsBySection.get(key).push(cmd);
25
+ }
26
+ const renderCommand = (command) => {
27
+ const isPinned = isCommandPinned(module.id, command.id);
28
+ const commandTitle = resolveLocalizedString(command.title, i18n.language);
29
+ return (_jsx(SidebarMenuItem, { className: "group/command", children: _jsxs("div", { className: "flex items-center w-full", children: [_jsxs(SidebarMenuButton, { onClick: () => navigate(command.route), isActive: isRouteActive(command.route), tooltip: commandTitle, "data-testid": `stacked-command-${command.id}`, className: "flex-1 min-w-0", children: [command.icon && _jsx(Icon, { name: command.icon, className: "h-4 w-4 shrink-0" }), _jsx("span", { className: "truncate", children: commandTitle })] }), showPinned && !isCollapsed && (_jsx("button", { onClick: (e) => {
30
+ e.stopPropagation();
31
+ e.preventDefault();
32
+ togglePin(module, command);
33
+ }, className: `shrink-0 p-0.5 rounded mr-2 transition-opacity ${isPinned
34
+ ? "text-primary hover:text-destructive hover:bg-sidebar-accent opacity-100"
35
+ : "text-muted-foreground hover:text-primary hover:bg-sidebar-accent opacity-0 group-hover/command:opacity-100"}`, title: isPinned ? "Unpin" : "Pin", "data-testid": `stacked-pin-toggle-${command.id}`, children: _jsx(Pin, { className: `h-3.5 w-3.5 ${isPinned ? "fill-current" : ""}` }) }))] }) }, command.id));
36
+ };
37
+ const unsectionedCommands = commandsBySection.get(undefined) ?? [];
38
+ return (_jsxs("div", { className: "flex flex-col h-full", "data-testid": "stacked-panel", children: [!isCollapsed && (_jsx("div", { className: "px-2 pt-2 pb-1", children: _jsxs("button", { onClick: () => navigate("/"), className: "flex items-center gap-1.5 w-full rounded-md px-2 py-1.5 text-sm font-medium text-sidebar-foreground hover:bg-sidebar-accent transition-colors", "data-testid": "stacked-back", children: [_jsx(ChevronLeft, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), module.icon && _jsx(Icon, { name: module.icon, className: "h-4 w-4 shrink-0" }), _jsx("span", { className: "truncate", children: moduleTitle })] }) })), _jsxs("div", { className: "flex-1 overflow-y-auto", children: [unsectionedCommands.length > 0 && (_jsx(SidebarGroup, { children: _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: unsectionedCommands.map(renderCommand) }) }) })), sections.map((section) => {
39
+ const sectionCommands = commandsBySection.get(section.id) ?? [];
40
+ if (sectionCommands.length === 0)
41
+ return null;
42
+ const sectionLabel = resolveLocalizedString(section.label, i18n.language);
43
+ return (_jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { className: "text-sidebar-foreground/90 font-semibold uppercase text-[10px] tracking-wider", "data-testid": `stacked-section-${section.id}`, children: sectionLabel }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: sectionCommands.map(renderCommand) }) })] }, section.id));
44
+ })] })] }));
45
+ }
@@ -1,8 +1,10 @@
1
+ import type { TimezoneMode } from "../../types/platform";
1
2
  export interface TopBarProps {
2
3
  onSearchClick?: () => void;
3
4
  environment?: string;
4
5
  locale?: string;
5
6
  onLocaleChange?: (locale: string) => void;
7
+ timezoneMode?: TimezoneMode;
8
+ onTimezoneToggle?: () => void;
6
9
  }
7
- export declare function TopBar({ onSearchClick, environment, locale: externalLocale, onLocaleChange, }: TopBarProps): import("react/jsx-runtime").JSX.Element;
8
- //# sourceMappingURL=TopBar.d.ts.map
10
+ export declare function TopBar({ onSearchClick, environment, locale: externalLocale, onLocaleChange, timezoneMode, onTimezoneToggle, }: TopBarProps): import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useEffect } from "react";
3
- import { Search, Moon, Sun, Check } from "lucide-react";
3
+ import { Search, Moon, Sun, Check, Clock, Globe } from "lucide-react";
4
4
  import { useTheme } from "./theme-provider";
5
5
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Badge, Button, } from "@nsxbet/admin-ui";
6
6
  import { SUPPORTED_LOCALES, LOCALE_FLAGS, LOCALE_NAMES } from "../../i18n";
@@ -27,7 +27,7 @@ const ENV_HEADER_COLORS_DARK = {
27
27
  staging: "bg-yellow-950 border-yellow-800",
28
28
  production: "bg-red-950 border-red-800",
29
29
  };
30
- export function TopBar({ onSearchClick, environment = "local", locale: externalLocale, onLocaleChange, }) {
30
+ export function TopBar({ onSearchClick, environment = "local", locale: externalLocale, onLocaleChange, timezoneMode = "local", onTimezoneToggle, }) {
31
31
  const { theme, setTheme } = useTheme();
32
32
  const { t } = useI18n();
33
33
  const [locale, setLocale] = useState(externalLocale || "pt-BR");
@@ -57,5 +57,11 @@ export function TopBar({ onSearchClick, environment = "local", locale: externalL
57
57
  : ENV_HEADER_COLORS_LIGHT[environment] || "bg-background border-b";
58
58
  // Get flag for current locale
59
59
  const currentFlag = LOCALE_FLAGS[locale] || "🌐";
60
- return (_jsxs("header", { className: `flex h-14 items-center justify-between px-4 ${headerColorClasses}`, role: "banner", "data-environment": environment, children: [_jsx(Badge, { className: `${ENV_COLORS[environment]} uppercase text-white`, children: environment }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsxs(Button, { variant: "outline", onClick: onSearchClick, "data-testid": "search-button", className: "relative h-8 w-40 justify-start rounded-md bg-muted/50 px-3 text-sm font-normal text-muted-foreground shadow-none sm:w-48", children: [_jsx(Search, { className: "mr-2 h-4 w-4" }), _jsx("span", { children: t('topBar.search') }), _jsxs("kbd", { className: "pointer-events-none absolute right-1.5 top-1 hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { className: "flex items-center gap-1 text-xl transition-opacity hover:opacity-80 focus:outline-none", "data-testid": "language-selector", title: t('topBar.language'), children: _jsx("span", { children: currentFlag }) }), _jsx(DropdownMenuContent, { align: "end", className: "min-w-[180px]", children: SUPPORTED_LOCALES.map((localeOption) => (_jsxs(DropdownMenuItem, { onClick: () => handleLocaleChange(localeOption), className: "flex items-center justify-between", "data-testid": `language-option-${localeOption}`, children: [_jsxs("div", { className: "flex items-center", children: [_jsx("span", { className: "mr-2 text-lg", children: LOCALE_FLAGS[localeOption] }), _jsx("span", { children: LOCALE_NAMES[localeOption] })] }), locale === localeOption && (_jsx(Check, { className: "h-4 w-4 text-primary" }))] }, localeOption))) })] }), _jsxs(Button, { variant: "ghost", size: "icon", onClick: toggleTheme, title: t('topBar.theme'), children: [_jsx(Sun, { className: "h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" }), _jsx(Moon, { className: "absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" }), _jsx("span", { className: "sr-only", children: t('topBar.theme') })] })] })] }));
60
+ return (_jsxs("header", { className: `flex h-14 items-center justify-between px-4 ${headerColorClasses}`, role: "banner", "data-environment": environment, children: [_jsx(Badge, { "data-testid": "environment-badge", className: `${ENV_COLORS[environment]} uppercase text-white`, children: environment }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsxs(Button, { variant: "outline", onClick: onSearchClick, "data-testid": "search-button", className: "relative h-8 w-40 justify-start rounded-md bg-muted/50 px-3 text-sm font-normal text-muted-foreground shadow-none sm:w-48", children: [_jsx(Search, { className: "mr-2 h-4 w-4" }), _jsx("span", { children: t('topBar.search') }), _jsxs("kbd", { className: "pointer-events-none absolute right-1.5 top-1 hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { className: "flex items-center gap-1 text-xl transition-opacity hover:opacity-80 focus:outline-none", "data-testid": "language-selector", title: t('topBar.language'), children: _jsx("span", { children: currentFlag }) }), _jsx(DropdownMenuContent, { align: "end", className: "min-w-[180px]", children: SUPPORTED_LOCALES.map((localeOption) => (_jsxs(DropdownMenuItem, { onClick: () => handleLocaleChange(localeOption), className: "flex items-center justify-between", "data-testid": `language-option-${localeOption}`, children: [_jsxs("div", { className: "flex items-center", children: [_jsx("span", { className: "mr-2 text-lg", children: LOCALE_FLAGS[localeOption] }), _jsx("span", { children: LOCALE_NAMES[localeOption] })] }), locale === localeOption && (_jsx(Check, { className: "h-4 w-4 text-primary" }))] }, localeOption))) })] }), _jsxs("div", { role: "radiogroup", "data-testid": "timezone-toggle", "aria-label": timezoneMode === "utc"
61
+ ? t('topBar.timezoneToggleUtc')
62
+ : t('topBar.timezoneToggleLocal'), className: "relative inline-flex h-8 items-center gap-0.5 rounded-lg border bg-muted/50 p-1 text-muted-foreground", children: [_jsx("div", { className: `absolute top-1 bottom-1 w-[calc(50%-4px)] rounded-md bg-background shadow-sm transition-transform duration-200 ease-out ${timezoneMode === "local" ? "translate-x-[calc(100%+2px)]" : "translate-x-0"}` }), _jsxs("button", { type: "button", role: "radio", "aria-checked": timezoneMode === "utc", onClick: timezoneMode !== "utc" ? onTimezoneToggle : undefined, className: `relative z-10 inline-flex items-center gap-1.5 rounded-md px-3 py-1 text-xs font-medium transition-colors duration-200 ${timezoneMode === "utc"
63
+ ? "text-foreground"
64
+ : "hover:text-foreground"}`, children: [_jsx(Globe, { className: "h-3.5 w-3.5" }), t('topBar.timezoneUtc')] }), _jsxs("button", { type: "button", role: "radio", "aria-checked": timezoneMode === "local", onClick: timezoneMode !== "local" ? onTimezoneToggle : undefined, className: `relative z-10 inline-flex items-center gap-1.5 rounded-md px-3 py-1 text-xs font-medium transition-colors duration-200 ${timezoneMode === "local"
65
+ ? "text-foreground"
66
+ : "hover:text-foreground"}`, children: [_jsx(Clock, { className: "h-3.5 w-3.5" }), t('topBar.timezoneLocal')] })] }), _jsxs(Button, { variant: "ghost", size: "icon", onClick: toggleTheme, title: t('topBar.theme'), children: [_jsx(Sun, { className: "h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" }), _jsx(Moon, { className: "absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" }), _jsx("span", { className: "sr-only", children: t('topBar.theme') })] })] })] }));
61
67
  }
@@ -0,0 +1,5 @@
1
+ export interface UpdateBannerProps {
2
+ onReload: () => void;
3
+ onDismiss: () => void;
4
+ }
5
+ export declare function UpdateBanner({ onReload, onDismiss }: UpdateBannerProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { RefreshCw, X } from "lucide-react";
3
+ import { Button } from "@nsxbet/admin-ui";
4
+ import { useI18n } from "../../hooks/useI18n";
5
+ export function UpdateBanner({ onReload, onDismiss }) {
6
+ const { t } = useI18n();
7
+ return (_jsxs("div", { className: "relative flex items-center justify-center gap-3 bg-blue-600 px-4 py-2 text-sm text-white dark:bg-blue-700", children: [_jsx(RefreshCw, { className: "h-4 w-4 shrink-0 animate-spin-slow" }), _jsx("span", { children: t("shell.updateBanner.message", "Updates available — Reload to apply") }), _jsx(Button, { variant: "secondary", size: "sm", className: "h-7 bg-white/20 text-white hover:bg-white/30", onClick: onReload, children: t("shell.updateBanner.reload", "Reload") }), _jsx("button", { onClick: onDismiss, className: "absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 text-white/70 hover:bg-white/20 hover:text-white", "aria-label": t("shell.updateBanner.dismiss", "Dismiss"), children: _jsx(X, { className: "h-4 w-4" }) })] }));
8
+ }
@@ -7,4 +7,7 @@ export type { CommandPaletteProps } from "./CommandPalette";
7
7
  export { ThemeProvider, useTheme } from "./theme-provider";
8
8
  export { RegistryPage } from "./RegistryPage";
9
9
  export { HomePage } from "./HomePage";
10
- //# sourceMappingURL=index.d.ts.map
10
+ export { RegistryStatusBanner } from "./RegistryStatusBanner";
11
+ export type { RegistryStatusBannerProps } from "./RegistryStatusBanner";
12
+ export { RegistryUnavailable } from "./RegistryUnavailable";
13
+ export type { RegistryUnavailableProps } from "./RegistryUnavailable";
@@ -5,3 +5,5 @@ export { CommandPalette } from "./CommandPalette";
5
5
  export { ThemeProvider, useTheme } from "./theme-provider";
6
6
  export { RegistryPage } from "./RegistryPage";
7
7
  export { HomePage } from "./HomePage";
8
+ export { RegistryStatusBanner } from "./RegistryStatusBanner";
9
+ export { RegistryUnavailable } from "./RegistryUnavailable";
@@ -12,4 +12,3 @@ export declare function ThemeProvider({ children, defaultTheme, // Brasa Design
12
12
  storageKey, ...props }: ThemeProviderProps): import("react/jsx-runtime").JSX.Element;
13
13
  export declare const useTheme: () => ThemeProviderState;
14
14
  export {};
15
- //# sourceMappingURL=theme-provider.d.ts.map
@@ -12,12 +12,15 @@ storageKey = "admin-ui-theme", ...props }) {
12
12
  const root = window.document.documentElement;
13
13
  root.classList.remove("light", "dark");
14
14
  if (theme === "system") {
15
- const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
16
- .matches
17
- ? "dark"
18
- : "light";
15
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
16
+ const systemTheme = mediaQuery.matches ? "dark" : "light";
19
17
  root.classList.add(systemTheme);
20
- return;
18
+ const handleChange = (e) => {
19
+ root.classList.remove("light", "dark");
20
+ root.classList.add(e.matches ? "dark" : "light");
21
+ };
22
+ mediaQuery.addEventListener("change", handleChange);
23
+ return () => mediaQuery.removeEventListener("change", handleChange);
21
24
  }
22
25
  root.classList.add(theme);
23
26
  }, [theme]);
@@ -0,0 +1,12 @@
1
+ export interface CspViolation {
2
+ blockedURI: string;
3
+ violatedDirective: string;
4
+ effectiveDirective: string;
5
+ documentURI: string;
6
+ timestamp: string;
7
+ }
8
+ export declare function useCspViolations(environment: string): {
9
+ violations: CspViolation[];
10
+ unseenCount: number;
11
+ resetUnseenCount: () => void;
12
+ };