@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
@@ -4,7 +4,9 @@ import { useNavigate, useLocation } from "react-router-dom";
4
4
  import { useAuth } from "../../hooks/useAuth";
5
5
  import { useAuthContext } from "../../components/AuthProvider";
6
6
  import { useI18n } from "../../hooks/useI18n";
7
+ import { resolveLocalizedString } from "../../i18n";
7
8
  import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarMenuSub, SidebarMenuSubItem, SidebarMenuSubButton, SidebarHeader, SidebarFooter, SidebarTrigger, useSidebar, Collapsible, CollapsibleContent, CollapsibleTrigger, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, Icon, ChevronRight, ChevronUp, ChevronDown, Lock, Pin, } from "@nsxbet/admin-ui";
9
+ import { StackedPanel } from "./StackedPanel";
8
10
  export function LeftNav({ modules }) {
9
11
  const navigate = useNavigate();
10
12
  const location = useLocation();
@@ -27,13 +29,13 @@ export function LeftNav({ modules }) {
27
29
  // Ignore localStorage errors
28
30
  }
29
31
  }, []);
30
- // Load pinned commands from localStorage
31
32
  useEffect(() => {
32
33
  const loadPinnedCommands = () => {
33
34
  try {
34
35
  const pinned = localStorage.getItem("adminPlatform.pinnedCommands");
35
36
  if (pinned) {
36
- setPinnedCommands(JSON.parse(pinned));
37
+ const parsed = JSON.parse(pinned);
38
+ setPinnedCommands(parsed.filter((p) => p.moduleId && p.commandId && p.route));
37
39
  }
38
40
  else {
39
41
  setPinnedCommands([]);
@@ -63,16 +65,13 @@ export function LeftNav({ modules }) {
63
65
  return () => window.removeEventListener("storage", handleStorageChange);
64
66
  }, []);
65
67
  const handleModuleClick = (module) => {
66
- // Navigate to the shell-rendered overview page
67
68
  navigate(`/_modules/${module.id}`);
68
69
  };
69
- // Helper to check if user has permission to view a module
70
70
  const checkModulePermission = (module) => {
71
71
  if (module.permissions.view.length === 0)
72
72
  return true;
73
73
  return module.permissions.view.some((perm) => auth.hasPermission(perm));
74
74
  };
75
- // Group modules by category (show all active modules, including those without permission)
76
75
  const modulesByCategory = useMemo(() => {
77
76
  const grouped = modules
78
77
  .filter((m) => m.status === "active")
@@ -88,93 +87,77 @@ export function LeftNav({ modules }) {
88
87
  acc[category].push(module);
89
88
  return acc;
90
89
  }, {});
91
- // Sort modules within each category by navOrder then title
92
90
  Object.keys(grouped).forEach((category) => {
93
91
  grouped[category].sort((a, b) => {
94
- if (a.navOrder !== undefined && b.navOrder !== undefined) {
95
- return a.navOrder - b.navOrder;
92
+ if (a.navigationOrder !== undefined && b.navigationOrder !== undefined) {
93
+ return a.navigationOrder - b.navigationOrder;
96
94
  }
97
- if (a.navOrder !== undefined)
95
+ if (a.navigationOrder !== undefined)
98
96
  return -1;
99
- if (b.navOrder !== undefined)
97
+ if (b.navigationOrder !== undefined)
100
98
  return 1;
101
- return a.title.localeCompare(b.title);
99
+ return resolveLocalizedString(a.title, i18n.language).localeCompare(resolveLocalizedString(b.title, i18n.language));
102
100
  });
103
101
  });
104
102
  return grouped;
105
- }, [modules, auth]);
106
- // Get sorted category names
107
- const categories = Object.keys(modulesByCategory).sort();
108
- // Permission denied tooltip message
103
+ }, [modules, auth, i18n.language]);
104
+ const categories = Object.keys(modulesByCategory).sort((a, b) => {
105
+ const minOrder = (cat) => {
106
+ const mods = modulesByCategory[cat];
107
+ return Math.min(...mods.map((m) => m.navigationOrder ?? Number.MAX_SAFE_INTEGER));
108
+ };
109
+ return minOrder(a) - minOrder(b);
110
+ });
109
111
  const noPermissionTooltip = t('errors.accessDeniedDescription');
110
- // Check if a route is active
111
112
  const isRouteActive = (route) => {
112
113
  return location.pathname === route || location.pathname.startsWith(route + "/");
113
114
  };
114
- // Helper to translate module/command titles
115
- // titleKey format: "namespace:key" (e.g., "tasks:module.title")
116
- const translateTitle = useCallback((title, titleKey) => {
117
- if (titleKey && titleKey.includes(':')) {
118
- // Parse "namespace:key" format
119
- const [ns, key] = titleKey.split(':');
120
- // Use i18n.t with explicit namespace option
121
- const translated = i18n.t(key, { ns, defaultValue: title });
122
- return translated;
123
- }
124
- if (titleKey) {
125
- // Fallback: try using the titleKey directly
126
- const translated = t(titleKey, { defaultValue: title });
127
- return translated;
128
- }
129
- return title;
130
- }, [i18n, t]);
131
- // Check if a command is pinned
132
115
  const isCommandPinned = useCallback((moduleId, commandId) => {
133
116
  return pinnedCommands.some((p) => p.moduleId === moduleId && p.commandId === commandId);
134
117
  }, [pinnedCommands]);
135
- // Toggle pin state for a command
118
+ const getPinnedDisplayTitles = useCallback((pinned) => {
119
+ const module = modules.find((m) => m.id === pinned.moduleId);
120
+ const command = module?.commands?.find((c) => c.id === pinned.commandId);
121
+ const moduleTitle = module
122
+ ? resolveLocalizedString(module.title, i18n.language)
123
+ : pinned.moduleId;
124
+ const commandTitle = command
125
+ ? resolveLocalizedString(command.title, i18n.language)
126
+ : pinned.commandId;
127
+ return { moduleTitle, commandTitle };
128
+ }, [modules, i18n.language]);
136
129
  const togglePin = useCallback((module, command) => {
137
130
  const isPinned = isCommandPinned(module.id, command.id);
138
131
  let updated;
139
132
  if (isPinned) {
140
- // Unpin
141
133
  updated = pinnedCommands.filter((p) => !(p.moduleId === module.id && p.commandId === command.id));
142
134
  }
143
135
  else {
144
- // Pin
145
136
  const pinnedCommand = {
146
137
  moduleId: module.id,
147
- moduleTitle: module.title,
148
- moduleTitleKey: module.titleKey,
149
- moduleIcon: module.icon,
150
138
  commandId: command.id,
151
- commandTitle: command.title,
152
- commandTitleKey: command.titleKey,
153
- commandIcon: command.icon,
154
139
  route: command.route,
140
+ moduleIcon: module.icon,
141
+ commandIcon: command.icon,
155
142
  };
156
143
  updated = [...pinnedCommands, pinnedCommand];
157
144
  }
158
145
  setPinnedCommands(updated);
159
146
  localStorage.setItem("adminPlatform.pinnedCommands", JSON.stringify(updated));
160
- // Notify other components by dispatching a storage event
161
147
  window.dispatchEvent(new StorageEvent("storage", {
162
148
  key: "adminPlatform.pinnedCommands",
163
149
  newValue: JSON.stringify(updated),
164
150
  }));
165
- }, [pinnedCommands, isCommandPinned]);
166
- // Unpin a command by moduleId and commandId
151
+ }, [pinnedCommands, isCommandPinned, modules]);
167
152
  const unpinCommand = useCallback((moduleId, commandId) => {
168
153
  const updated = pinnedCommands.filter((p) => !(p.moduleId === moduleId && p.commandId === commandId));
169
154
  setPinnedCommands(updated);
170
155
  localStorage.setItem("adminPlatform.pinnedCommands", JSON.stringify(updated));
171
- // Notify other components by dispatching a storage event
172
156
  window.dispatchEvent(new StorageEvent("storage", {
173
157
  key: "adminPlatform.pinnedCommands",
174
158
  newValue: JSON.stringify(updated),
175
159
  }));
176
160
  }, [pinnedCommands]);
177
- // Move a pinned item up in the list
178
161
  const movePinnedUp = useCallback((index) => {
179
162
  if (index <= 0)
180
163
  return;
@@ -187,7 +170,6 @@ export function LeftNav({ modules }) {
187
170
  newValue: JSON.stringify(updated),
188
171
  }));
189
172
  }, [pinnedCommands]);
190
- // Move a pinned item down in the list
191
173
  const movePinnedDown = useCallback((index) => {
192
174
  if (index >= pinnedCommands.length - 1)
193
175
  return;
@@ -200,48 +182,64 @@ export function LeftNav({ modules }) {
200
182
  newValue: JSON.stringify(updated),
201
183
  }));
202
184
  }, [pinnedCommands]);
203
- return (_jsxs(Sidebar, { collapsible: "icon", "data-testid": "left-nav", children: [_jsx(SidebarHeader, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsx(SidebarTrigger, { "data-testid": "sidebar-toggle" }) }) }) }), _jsxs(SidebarContent, { children: [showPinned && pinnedCommands.length > 0 && (_jsxs(SidebarGroup, { "data-testid": "pinned-section", children: [_jsxs(SidebarGroupLabel, { className: "text-sidebar-foreground/90 font-semibold uppercase text-[10px] tracking-wider", children: [_jsx(Pin, { className: "mr-2 h-4 w-4" }), !isCollapsed && t('nav.pinned')] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: pinnedCommands.map((pinned, index) => {
204
- const iconName = pinned.commandIcon || pinned.moduleIcon || "file-text";
205
- const isFirst = index === 0;
206
- const isLast = index === pinnedCommands.length - 1;
207
- return (_jsx(SidebarMenuItem, { className: "group/pinned", children: _jsxs("div", { className: "flex items-center w-full", children: [_jsxs(SidebarMenuButton, { onClick: () => navigate(pinned.route), isActive: isRouteActive(pinned.route), tooltip: `${translateTitle(pinned.moduleTitle, pinned.moduleTitleKey)} / ${translateTitle(pinned.commandTitle, pinned.commandTitleKey)}`, "data-testid": `pinned-${pinned.commandId}`, className: "flex-1 min-w-0", children: [_jsx(Icon, { name: iconName, className: "h-4 w-4 shrink-0" }), _jsxs("span", { className: "truncate", children: [translateTitle(pinned.moduleTitle, pinned.moduleTitleKey), " / ", translateTitle(pinned.commandTitle, pinned.commandTitleKey)] })] }), !isCollapsed && (_jsxs("div", { className: "flex items-center gap-0.5 shrink-0 pr-2 opacity-0 group-hover/pinned:opacity-100 transition-opacity", children: [!isFirst && (_jsx("button", { onClick: (e) => {
208
- e.stopPropagation();
209
- e.preventDefault();
210
- movePinnedUp(index);
211
- }, className: "hover:text-primary p-0.5 rounded hover:bg-sidebar-accent text-muted-foreground", title: "Move up", "data-testid": `move-up-${pinned.commandId}`, children: _jsx(ChevronUp, { className: "h-3.5 w-3.5" }) })), !isLast && (_jsx("button", { onClick: (e) => {
212
- e.stopPropagation();
213
- e.preventDefault();
214
- movePinnedDown(index);
215
- }, className: "hover:text-primary p-0.5 rounded hover:bg-sidebar-accent text-muted-foreground", title: "Move down", "data-testid": `move-down-${pinned.commandId}`, children: _jsx(ChevronDown, { className: "h-3.5 w-3.5" }) })), _jsx("button", { onClick: (e) => {
216
- e.stopPropagation();
217
- e.preventDefault();
218
- unpinCommand(pinned.moduleId, pinned.commandId);
219
- }, className: "hover:text-destructive p-0.5 rounded hover:bg-sidebar-accent text-muted-foreground", title: "Unpin", "data-testid": `unpin-${pinned.commandId}`, children: _jsx(Pin, { className: "h-3.5 w-3.5 fill-current" }) })] }))] }) }, `${pinned.moduleId}:${pinned.commandId}`));
220
- }) }) })] })), categories.map((category) => (_jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { "data-testid": `category-${category}`, className: "text-sidebar-foreground/90 font-semibold uppercase text-[10px] tracking-wider", children: category }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { "data-testid": `category-modules-${category}`, children: modulesByCategory[category].map((module) => {
221
- const hasCommands = module.commands && module.commands.length > 0;
222
- const isDisabled = !module.hasViewPermission;
223
- if (!hasCommands) {
224
- // Simple button for modules without commands
225
- const moduleTitle = translateTitle(module.title, module.titleKey);
226
- return (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { onClick: () => !isDisabled && handleModuleClick(module), disabled: isDisabled, tooltip: isDisabled ? noPermissionTooltip : moduleTitle, "data-testid": `module-${module.id}`, children: [module.icon && _jsx(Icon, { name: module.icon, className: "h-4 w-4" }), _jsx("span", { className: "truncate", children: moduleTitle }), isDisabled && _jsx(Lock, { className: "ml-auto h-3 w-3 text-muted-foreground" })] }) }, module.id));
227
- }
228
- // Disabled module with commands - show as non-interactive
229
- if (isDisabled) {
230
- const disabledModuleTitle = translateTitle(module.title, module.titleKey);
231
- return (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { disabled: true, tooltip: noPermissionTooltip, "data-testid": `module-${module.id}`, children: [module.icon && _jsx(Icon, { name: module.icon, className: "h-4 w-4" }), _jsx("span", { className: "truncate", children: disabledModuleTitle }), _jsx(Lock, { className: "ml-auto h-3 w-3 text-muted-foreground" })] }) }, module.id));
232
- }
233
- // Module with commands - collapsible
234
- const collapsibleModuleTitle = translateTitle(module.title, module.titleKey);
235
- return (_jsx(Collapsible, { asChild: true, className: "group/collapsible", children: _jsxs(SidebarMenuItem, { children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { tooltip: collapsibleModuleTitle, "data-testid": `module-${module.id}-trigger`, children: [module.icon && _jsx(Icon, { name: module.icon, className: "h-4 w-4" }), _jsx("span", { className: "truncate", children: collapsibleModuleTitle }), _jsx(ChevronRight, { className: "ml-auto h-4 w-4 transition-transform group-data-[state=open]/collapsible:rotate-90" })] }) }), _jsx(CollapsibleContent, { children: _jsxs(SidebarMenuSub, { children: [_jsx(SidebarMenuSubItem, { children: _jsx(SidebarMenuSubButton, { onClick: () => handleModuleClick(module), isActive: location.pathname === `/_modules/${module.id}`, "data-testid": `module-${module.id}`, children: t('nav.overview') }) }), module.commands?.map((command) => {
236
- const isPinned = isCommandPinned(module.id, command.id);
237
- const commandTitle = translateTitle(command.title, command.titleKey);
238
- return (_jsx(SidebarMenuSubItem, { className: "group/command", children: _jsxs("div", { className: "flex items-center w-full", children: [_jsx(SidebarMenuSubButton, { onClick: () => navigate(command.route), isActive: isRouteActive(command.route), "data-testid": `command-${command.id}`, className: "flex-1 min-w-0", children: _jsx("span", { className: "truncate", children: commandTitle }) }), showPinned && (_jsx("button", { onClick: (e) => {
239
- e.stopPropagation();
240
- e.preventDefault();
241
- togglePin(module, command);
242
- }, className: `shrink-0 p-0.5 rounded mr-2 transition-opacity ${isPinned
243
- ? "text-primary hover:text-destructive hover:bg-sidebar-accent opacity-100"
244
- : "text-muted-foreground hover:text-primary hover:bg-sidebar-accent opacity-0 group-hover/command:opacity-100"}`, title: isPinned ? "Unpin" : "Pin", "data-testid": `pin-toggle-${command.id}`, children: _jsx(Pin, { className: `h-3.5 w-3.5 ${isPinned ? "fill-current" : ""}` }) }))] }) }, command.id));
245
- })] }) })] }) }, module.id));
246
- }) }) })] }, category)))] }), _jsx(SidebarFooter, { className: `border-t border-sidebar-border ${isCollapsed ? "items-center" : ""}`, children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", tooltip: user?.displayName || "User", "data-testid": "user-menu-trigger", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", children: user?.displayName?.charAt(0).toUpperCase() || "U" }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.displayName || "User" }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email || "" })] }), _jsx(ChevronUp, { className: "ml-auto" })] }) }), _jsxs(DropdownMenuContent, { side: isCollapsed ? "right" : "top", align: isCollapsed ? "end" : "start", className: "w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg", children: [_jsxs(DropdownMenuItem, { onClick: () => navigate("/_profile"), className: isRouteActive("/_profile") ? "bg-accent" : "", "data-testid": "nav-profile", children: [_jsx(Icon, { name: "user", className: "h-4 w-4 mr-2" }), t('nav.profile')] }), _jsxs(DropdownMenuItem, { onClick: () => navigate("/_settings"), className: isRouteActive("/_settings") ? "bg-accent" : "", "data-testid": "nav-settings", children: [_jsx(Icon, { name: "settings", className: "h-4 w-4 mr-2" }), t('nav.settings')] }), _jsxs(DropdownMenuItem, { onClick: () => navigate("/_registry"), className: isRouteActive("/_registry") ? "bg-accent" : "", "data-testid": "nav-registry", children: [_jsx(Icon, { name: "package", className: "h-4 w-4 mr-2" }), t('nav.registry')] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: () => logout(), "data-testid": "nav-logout", children: [_jsx(Icon, { name: "log-out", className: "h-4 w-4 mr-2" }), t('nav.logout')] })] })] }) }) }) })] }));
185
+ // URL-driven stacked panel detection
186
+ const activeStackedModule = useMemo(() => {
187
+ return modules.find((m) => m.navigation?.style === "stacked" &&
188
+ m.status === "active" &&
189
+ (location.pathname === m.routeBase ||
190
+ location.pathname.startsWith(m.routeBase + "/"))) ?? null;
191
+ }, [modules, location.pathname]);
192
+ const isStacked = activeStackedModule !== null;
193
+ const isStackedModule = (module) => module.navigation?.style === "stacked";
194
+ const userFooter = (_jsx(SidebarFooter, { className: `border-t border-sidebar-border ${isCollapsed ? "items-center" : ""}`, children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", tooltip: user?.displayName || "User", "data-testid": "user-menu-trigger", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", children: user?.displayName?.charAt(0).toUpperCase() || "U" }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.displayName || "User" }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email || "" })] }), _jsx(ChevronUp, { className: "ml-auto" })] }) }), _jsxs(DropdownMenuContent, { side: isCollapsed ? "right" : "top", align: isCollapsed ? "end" : "start", className: "w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg", children: [_jsxs(DropdownMenuItem, { onClick: () => navigate("/_profile"), className: isRouteActive("/_profile") ? "bg-accent" : "", "data-testid": "nav-profile", children: [_jsx(Icon, { name: "user", className: "h-4 w-4 mr-2" }), t('navigation.profile')] }), _jsxs(DropdownMenuItem, { onClick: () => navigate("/_settings"), className: isRouteActive("/_settings") ? "bg-accent" : "", "data-testid": "nav-settings", children: [_jsx(Icon, { name: "settings", className: "h-4 w-4 mr-2" }), t('navigation.settings')] }), auth.hasPermission('admin.platform.view') && (_jsxs(DropdownMenuItem, { onClick: () => navigate("/_registry"), className: isRouteActive("/_registry") ? "bg-accent" : "", "data-testid": "nav-registry", children: [_jsx(Icon, { name: "package", className: "h-4 w-4 mr-2" }), t('navigation.registry')] })), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: () => logout(), "data-testid": "nav-logout", children: [_jsx(Icon, { name: "log-out", className: "h-4 w-4 mr-2" }), t('navigation.logout')] })] })] }) }) }) }));
195
+ return (_jsx(Sidebar, { collapsible: "icon", "data-testid": "left-nav", children: _jsxs("div", { className: "flex h-full flex-col overflow-hidden", children: [_jsx(SidebarHeader, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsx(SidebarTrigger, { "data-testid": "sidebar-toggle" }) }) }) }), _jsxs("div", { className: "flex flex-1 min-h-0 transition-transform duration-200 ease-in-out motion-reduce:transition-none", style: {
196
+ width: "200%",
197
+ transform: isStacked && !isCollapsed ? "translateX(-50%)" : "translateX(0)",
198
+ }, children: [_jsx("div", { className: "w-1/2 flex flex-col min-h-0", children: _jsxs(SidebarContent, { className: "flex-1 min-h-0", children: [showPinned && pinnedCommands.length > 0 && (_jsxs(SidebarGroup, { "data-testid": "pinned-section", children: [_jsxs(SidebarGroupLabel, { className: "text-sidebar-foreground/90 font-semibold uppercase text-[10px] tracking-wider", children: [_jsx(Pin, { className: "mr-2 h-4 w-4" }), !isCollapsed && t('navigation.pinned')] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: pinnedCommands.map((pinned, index) => {
199
+ const iconName = pinned.commandIcon || pinned.moduleIcon || "file-text";
200
+ const isFirst = index === 0;
201
+ const isLast = index === pinnedCommands.length - 1;
202
+ return (_jsx(SidebarMenuItem, { className: "group/pinned", children: _jsxs("div", { className: "flex items-center w-full", children: [_jsxs(SidebarMenuButton, { onClick: () => navigate(pinned.route), isActive: isRouteActive(pinned.route), tooltip: `${getPinnedDisplayTitles(pinned).moduleTitle} / ${getPinnedDisplayTitles(pinned).commandTitle}`, "data-testid": `pinned-${pinned.commandId}`, className: "flex-1 min-w-0", children: [_jsx(Icon, { name: iconName, className: "h-4 w-4 shrink-0" }), _jsxs("span", { className: "truncate", children: [getPinnedDisplayTitles(pinned).moduleTitle, " / ", getPinnedDisplayTitles(pinned).commandTitle] })] }), !isCollapsed && (_jsxs("div", { className: "flex items-center gap-0.5 shrink-0 pr-2 opacity-0 group-hover/pinned:opacity-100 transition-opacity", children: [!isFirst && (_jsx("button", { onClick: (e) => {
203
+ e.stopPropagation();
204
+ e.preventDefault();
205
+ movePinnedUp(index);
206
+ }, className: "hover:text-primary p-0.5 rounded hover:bg-sidebar-accent text-muted-foreground", title: "Move up", "data-testid": `move-up-${pinned.commandId}`, children: _jsx(ChevronUp, { className: "h-3.5 w-3.5" }) })), !isLast && (_jsx("button", { onClick: (e) => {
207
+ e.stopPropagation();
208
+ e.preventDefault();
209
+ movePinnedDown(index);
210
+ }, className: "hover:text-primary p-0.5 rounded hover:bg-sidebar-accent text-muted-foreground", title: "Move down", "data-testid": `move-down-${pinned.commandId}`, children: _jsx(ChevronDown, { className: "h-3.5 w-3.5" }) })), _jsx("button", { onClick: (e) => {
211
+ e.stopPropagation();
212
+ e.preventDefault();
213
+ unpinCommand(pinned.moduleId, pinned.commandId);
214
+ }, className: "hover:text-destructive p-0.5 rounded hover:bg-sidebar-accent text-muted-foreground", title: "Unpin", "data-testid": `unpin-${pinned.commandId}`, children: _jsx(Pin, { className: "h-3.5 w-3.5 fill-current" }) })] }))] }) }, `${pinned.moduleId}:${pinned.commandId}`));
215
+ }) }) })] })), categories.map((category) => (_jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { "data-testid": `category-${category}`, className: "text-sidebar-foreground/90 font-semibold uppercase text-[10px] tracking-wider", children: category }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { "data-testid": `category-modules-${category}`, children: modulesByCategory[category].map((module) => {
216
+ const hasCommands = module.commands && module.commands.length > 0;
217
+ const isDisabled = !module.hasViewPermission;
218
+ if (!hasCommands) {
219
+ const moduleTitle = resolveLocalizedString(module.title, i18n.language);
220
+ return (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { onClick: () => !isDisabled && handleModuleClick(module), className: isDisabled ? "opacity-50 cursor-not-allowed" : "", tooltip: isDisabled ? noPermissionTooltip : moduleTitle, "data-testid": `module-${module.id}`, children: [module.icon && _jsx(Icon, { name: module.icon, className: "h-4 w-4" }), _jsx("span", { className: "truncate", children: moduleTitle }), isDisabled && _jsx(Lock, { className: "ml-auto h-3 w-3 text-muted-foreground" })] }) }, module.id));
221
+ }
222
+ if (isDisabled) {
223
+ const disabledModuleTitle = resolveLocalizedString(module.title, i18n.language);
224
+ return (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { className: "opacity-50 cursor-not-allowed", tooltip: noPermissionTooltip, "data-testid": `module-${module.id}`, children: [module.icon && _jsx(Icon, { name: module.icon, className: "h-4 w-4" }), _jsx("span", { className: "truncate", children: disabledModuleTitle }), _jsx(Lock, { className: "ml-auto h-3 w-3 text-muted-foreground" })] }) }, module.id));
225
+ }
226
+ // Stacked module: single button with chevron-right
227
+ if (isStackedModule(module)) {
228
+ const stackedModuleTitle = resolveLocalizedString(module.title, i18n.language);
229
+ return (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { onClick: () => navigate(module.routeBase), isActive: isRouteActive(module.routeBase), tooltip: stackedModuleTitle, "data-testid": `module-${module.id}`, children: [module.icon && _jsx(Icon, { name: module.icon, className: "h-4 w-4" }), _jsx("span", { className: "truncate", children: stackedModuleTitle }), _jsx(ChevronRight, { className: "ml-auto h-4 w-4" })] }) }, module.id));
230
+ }
231
+ // Collapsible module (default)
232
+ const collapsibleModuleTitle = resolveLocalizedString(module.title, i18n.language);
233
+ return (_jsx(Collapsible, { asChild: true, className: "group/collapsible", children: _jsxs(SidebarMenuItem, { children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { tooltip: collapsibleModuleTitle, "data-testid": `module-${module.id}-trigger`, children: [module.icon && _jsx(Icon, { name: module.icon, className: "h-4 w-4" }), _jsx("span", { className: "truncate", children: collapsibleModuleTitle }), _jsx(ChevronRight, { className: "ml-auto h-4 w-4 transition-transform group-data-[state=open]/collapsible:rotate-90" })] }) }), _jsx(CollapsibleContent, { children: _jsxs(SidebarMenuSub, { children: [_jsx(SidebarMenuSubItem, { children: _jsx(SidebarMenuSubButton, { onClick: () => handleModuleClick(module), isActive: location.pathname === `/_modules/${module.id}`, "data-testid": `module-${module.id}`, children: t('navigation.overview') }) }), module.commands?.map((command) => {
234
+ const isPinned = isCommandPinned(module.id, command.id);
235
+ const commandTitle = resolveLocalizedString(command.title, i18n.language);
236
+ return (_jsx(SidebarMenuSubItem, { className: "group/command", children: _jsxs("div", { className: "flex items-center w-full", children: [_jsx(SidebarMenuSubButton, { onClick: () => navigate(command.route), isActive: isRouteActive(command.route), "data-testid": `command-${command.id}`, className: "flex-1 min-w-0", children: _jsx("span", { className: "truncate", children: commandTitle }) }), showPinned && (_jsx("button", { onClick: (e) => {
237
+ e.stopPropagation();
238
+ e.preventDefault();
239
+ togglePin(module, command);
240
+ }, className: `shrink-0 p-0.5 rounded mr-2 transition-opacity ${isPinned
241
+ ? "text-primary hover:text-destructive hover:bg-sidebar-accent opacity-100"
242
+ : "text-muted-foreground hover:text-primary hover:bg-sidebar-accent opacity-0 group-hover/command:opacity-100"}`, title: isPinned ? "Unpin" : "Pin", "data-testid": `pin-toggle-${command.id}`, children: _jsx(Pin, { className: `h-3.5 w-3.5 ${isPinned ? "fill-current" : ""}` }) }))] }) }, command.id));
243
+ })] }) })] }) }, module.id));
244
+ }) }) })] }, category))), auth.hasPermission('admin.platform.view') && (_jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { className: "text-sidebar-foreground/90 font-semibold uppercase text-[10px] tracking-wider", children: t('navigation.platform') }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { onClick: () => navigate("/_registry"), isActive: isRouteActive("/_registry"), tooltip: t('navigation.registry'), "data-testid": "nav-registry-sidebar", children: [_jsx(Icon, { name: "package", className: "h-4 w-4" }), _jsx("span", { className: "truncate", children: t('navigation.registry') })] }) }) }) })] }))] }) }), _jsx("div", { className: "w-1/2 flex flex-col min-h-0", children: activeStackedModule ? (_jsx(StackedPanel, { module: activeStackedModule, showPinned: showPinned, isCommandPinned: isCommandPinned, togglePin: togglePin })) : null })] }), userFooter] }) }));
247
245
  }
@@ -1,9 +1,10 @@
1
1
  import { type ReactNode } from "react";
2
2
  import type { Module } from "../types";
3
+ import type { Breadcrumb } from "../../types/platform";
3
4
  interface MainContentProps {
4
5
  modules: Module[];
5
6
  children?: ReactNode;
7
+ moduleBreadcrumbs?: Breadcrumb[] | null;
6
8
  }
7
- export declare function MainContent({ modules, children }: MainContentProps): import("react/jsx-runtime").JSX.Element;
9
+ export declare function MainContent({ modules, children, moduleBreadcrumbs }: MainContentProps): import("react/jsx-runtime").JSX.Element;
8
10
  export {};
9
- //# sourceMappingURL=MainContent.d.ts.map
@@ -1,29 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Fragment, useState, useEffect, useCallback } from "react";
2
+ import { Fragment, useState, useEffect } from "react";
3
3
  import { useLocation, useNavigate } from "react-router-dom";
4
4
  import { useI18n } from "../../hooks/useI18n";
5
- export function MainContent({ modules, children }) {
5
+ import { resolveLocalizedString } from "../../i18n";
6
+ export function MainContent({ modules, children, moduleBreadcrumbs }) {
6
7
  const location = useLocation();
7
8
  const navigate = useNavigate();
8
9
  const { t, i18n } = useI18n();
9
10
  const [breadcrumbs, setBreadcrumbs] = useState([]);
10
- // Helper to translate module/command titles
11
- // titleKey format: "namespace:key" (e.g., "tasks:module.title")
12
- const translateTitle = useCallback((title, titleKey) => {
13
- if (titleKey && titleKey.includes(':')) {
14
- // Parse "namespace:key" format
15
- const [ns, key] = titleKey.split(':');
16
- // Use i18n.t with explicit namespace option
17
- const translated = i18n.t(key, { ns, defaultValue: title });
18
- return translated;
19
- }
20
- if (titleKey) {
21
- // Fallback: try using the titleKey directly
22
- const translated = t(titleKey, { defaultValue: title });
23
- return translated;
24
- }
25
- return title;
26
- }, [i18n, t]);
27
11
  // Auto-detect module and update breadcrumbs based on current route
28
12
  useEffect(() => {
29
13
  const currentPath = location.pathname;
@@ -55,10 +39,10 @@ export function MainContent({ modules, children }) {
55
39
  const moduleHref = module.commands && module.commands.length > 0
56
40
  ? module.commands[0].route
57
41
  : module.routeBase;
58
- crumbs.push({ label: translateTitle(module.title, module.titleKey), href: moduleHref });
42
+ crumbs.push({ label: resolveLocalizedString(module.title, i18n.language), href: moduleHref });
59
43
  if (command) {
60
44
  // Add command as breadcrumb (with translation)
61
- crumbs.push({ label: translateTitle(command.title, command.titleKey) });
45
+ crumbs.push({ label: resolveLocalizedString(command.title, i18n.language) });
62
46
  }
63
47
  else {
64
48
  // Add sub-paths if any (for non-command routes)
@@ -83,6 +67,7 @@ export function MainContent({ modules, children }) {
83
67
  // No module matched, show home
84
68
  setBreadcrumbs([{ label: t('breadcrumbs.home') }]);
85
69
  }
86
- }, [location.pathname, modules, t, translateTitle]);
87
- return (_jsxs("main", { className: "flex flex-1 flex-col overflow-hidden", children: [_jsx("div", { "data-testid": "breadcrumbs", className: "flex h-10 items-center justify-between border-b px-4 text-sm text-muted-foreground", children: _jsx("div", { className: "flex items-center gap-2", children: breadcrumbs.map((crumb, index) => (_jsxs(Fragment, { children: [index > 0 && _jsx("span", { children: "/" }), crumb.href ? (_jsx("button", { onClick: () => navigate(crumb.href), className: "transition-colors hover:text-foreground", children: crumb.label })) : (_jsx("span", { className: "text-foreground", children: crumb.label }))] }, index))) }) }), _jsx("div", { className: "flex-1 overflow-auto p-4", "data-testid": "module-container", children: children })] }));
70
+ }, [location.pathname, modules, i18n.language]);
71
+ const displayBreadcrumbs = moduleBreadcrumbs ?? breadcrumbs;
72
+ return (_jsxs("main", { className: "flex flex-1 flex-col overflow-hidden", children: [_jsx("div", { "data-testid": "breadcrumbs", className: "flex h-10 items-center justify-between border-b px-4 text-sm text-muted-foreground", children: _jsx("div", { className: "flex items-center gap-2", children: displayBreadcrumbs.map((crumb, index) => (_jsxs(Fragment, { children: [index > 0 && _jsx("span", { children: "/" }), crumb.href ? (_jsx("button", { onClick: () => navigate(crumb.href), className: "transition-colors hover:text-foreground", children: crumb.label })) : (_jsx("span", { className: "text-foreground", children: crumb.label }))] }, index))) }) }), _jsx("div", { className: "flex-1 overflow-auto p-4", "data-testid": "module-container", children: children })] }));
88
73
  }
@@ -4,4 +4,3 @@ interface ModuleOverviewProps {
4
4
  }
5
5
  export declare function ModuleOverview({ modules }: ModuleOverviewProps): import("react/jsx-runtime").JSX.Element;
6
6
  export {};
7
- //# sourceMappingURL=ModuleOverview.d.ts.map
@@ -2,27 +2,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useLocation, useNavigate } from "react-router-dom";
3
3
  import { Card, CardContent, CardDescription, CardHeader, CardTitle, Badge, Icon, } from "@nsxbet/admin-ui";
4
4
  import { useI18n } from "../../hooks/useI18n";
5
+ import { resolveLocalizedString } from "../../i18n";
5
6
  export function ModuleOverview({ modules }) {
6
7
  const location = useLocation();
7
8
  const navigate = useNavigate();
8
9
  const { t, i18n } = useI18n();
9
- // Helper to translate titles
10
- // titleKey format: "namespace:key" (e.g., "tasks:module.title")
11
- const translateTitle = (title, titleKey) => {
12
- if (titleKey && titleKey.includes(':')) {
13
- // Parse "namespace:key" format
14
- const [ns, key] = titleKey.split(':');
15
- // Use i18n.t with explicit namespace option
16
- const translated = i18n.t(key, { ns, defaultValue: title });
17
- return translated;
18
- }
19
- if (titleKey) {
20
- // Fallback: try using the titleKey directly
21
- const translated = t(titleKey, { defaultValue: title });
22
- return translated;
23
- }
24
- return title;
25
- };
26
10
  // Extract moduleId from path: /_modules/xxx -> xxx
27
11
  const moduleId = location.pathname.replace(/^\/_modules\//, "");
28
12
  const module = modules.find((m) => m.id === moduleId);
@@ -34,7 +18,7 @@ export function ModuleOverview({ modules }) {
34
18
  : module.status === "deprecated"
35
19
  ? "warning"
36
20
  : "secondary";
37
- const moduleTitle = translateTitle(module.title, module.titleKey);
38
- const moduleDescription = translateTitle(module.description, module.descriptionKey);
39
- return (_jsxs("div", { className: "p-6 max-w-3xl mx-auto space-y-6", children: [_jsxs("div", { className: "flex items-start gap-4", children: [_jsx("div", { className: "flex-shrink-0 p-3 rounded-lg bg-muted", children: _jsx(Icon, { name: module.icon || "package", className: "h-8 w-8" }) }), _jsxs("div", { className: "flex-1", children: [_jsxs("div", { className: "flex items-center gap-3 mb-1", children: [_jsx("h1", { className: "text-3xl font-bold", children: moduleTitle }), _jsx(Badge, { variant: statusVariant, className: "capitalize", children: module.status })] }), moduleDescription && (_jsx("p", { className: "text-muted-foreground", children: moduleDescription }))] })] }), module.commands && module.commands.length > 0 && (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "zap", className: "h-5 w-5 text-muted-foreground" }), _jsx(CardTitle, { className: "text-lg", children: t('moduleOverview.actions') })] }), _jsx(CardDescription, { children: t('moduleOverview.actionsDescription') })] }), _jsx(CardContent, { children: _jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: module.commands.map((command) => (_jsxs("button", { onClick: () => navigate(command.route), className: "flex items-center gap-3 rounded-lg border border-border bg-card p-4 text-left transition-colors hover:bg-accent hover:border-accent", children: [_jsx("div", { className: "flex-shrink-0 p-2 rounded-md bg-muted", children: _jsx(Icon, { name: command.icon || "file-text", className: "h-5 w-5" }) }), _jsx("span", { className: "font-medium", children: translateTitle(command.title, command.titleKey) })] }, command.id))) }) })] })), _jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "info", className: "h-5 w-5 text-muted-foreground" }), _jsx(CardTitle, { className: "text-lg", children: t('moduleOverview.information') })] }) }), _jsx(CardContent, { children: _jsxs("dl", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between py-2 border-b border-border", children: [_jsxs("dt", { className: "text-sm font-medium text-muted-foreground flex items-center gap-2", children: [_jsx(Icon, { name: "folder", className: "h-4 w-4" }), t('moduleOverview.category')] }), _jsx("dd", { className: "text-sm font-medium", children: module.category })] }), _jsxs("div", { className: "flex items-center justify-between py-2 border-b border-border", children: [_jsxs("dt", { className: "text-sm font-medium text-muted-foreground flex items-center gap-2", children: [_jsx(Icon, { name: "activity", className: "h-4 w-4" }), t('moduleOverview.status')] }), _jsx("dd", { children: _jsx(Badge, { variant: statusVariant, className: "capitalize", children: module.status }) })] }), module.owners?.team && (_jsxs("div", { className: "flex items-center justify-between py-2 border-b border-border", children: [_jsxs("dt", { className: "text-sm font-medium text-muted-foreground flex items-center gap-2", children: [_jsx(Icon, { name: "users", className: "h-4 w-4" }), t('moduleOverview.owner')] }), _jsx("dd", { className: "text-sm font-medium", children: module.owners.team })] })), module.owners?.supportChannel && (_jsxs("div", { className: "flex items-center justify-between py-2", children: [_jsxs("dt", { className: "text-sm font-medium text-muted-foreground flex items-center gap-2", children: [_jsx(Icon, { name: "message-circle", className: "h-4 w-4" }), t('moduleOverview.support')] }), _jsx("dd", { className: "text-sm font-medium", children: module.owners.supportChannel })] }))] }) })] })] }));
21
+ const moduleTitle = resolveLocalizedString(module.title, i18n.language);
22
+ const moduleDescription = resolveLocalizedString(module.description, i18n.language);
23
+ return (_jsxs("div", { className: "p-6 max-w-3xl mx-auto space-y-6", children: [_jsxs("div", { className: "flex items-start gap-4", children: [_jsx("div", { className: "flex-shrink-0 p-3 rounded-lg bg-muted", children: _jsx(Icon, { name: module.icon || "package", className: "h-8 w-8" }) }), _jsxs("div", { className: "flex-1", children: [_jsxs("div", { className: "flex items-center gap-3 mb-1", children: [_jsx("h1", { className: "text-3xl font-bold", children: moduleTitle }), _jsx(Badge, { variant: statusVariant, className: "capitalize", children: module.status })] }), moduleDescription && (_jsx("p", { className: "text-muted-foreground", children: moduleDescription }))] })] }), module.commands && module.commands.length > 0 && (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "zap", className: "h-5 w-5 text-muted-foreground" }), _jsx(CardTitle, { className: "text-lg", children: t('moduleOverview.actions') })] }), _jsx(CardDescription, { children: t('moduleOverview.actionsDescription') })] }), _jsx(CardContent, { children: _jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: module.commands.map((command) => (_jsxs("button", { onClick: () => navigate(command.route), className: "flex items-center gap-3 rounded-lg border border-border bg-card p-4 text-left transition-colors hover:bg-accent hover:border-accent", children: [_jsx("div", { className: "flex-shrink-0 p-2 rounded-md bg-muted", children: _jsx(Icon, { name: command.icon || "file-text", className: "h-5 w-5" }) }), _jsx("span", { className: "font-medium", children: resolveLocalizedString(command.title, i18n.language) })] }, command.id))) }) })] })), _jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "info", className: "h-5 w-5 text-muted-foreground" }), _jsx(CardTitle, { className: "text-lg", children: t('moduleOverview.information') })] }) }), _jsx(CardContent, { children: _jsxs("dl", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between py-2 border-b border-border", children: [_jsxs("dt", { className: "text-sm font-medium text-muted-foreground flex items-center gap-2", children: [_jsx(Icon, { name: "folder", className: "h-4 w-4" }), t('moduleOverview.category')] }), _jsx("dd", { className: "text-sm font-medium", children: module.category })] }), _jsxs("div", { className: "flex items-center justify-between py-2 border-b border-border", children: [_jsxs("dt", { className: "text-sm font-medium text-muted-foreground flex items-center gap-2", children: [_jsx(Icon, { name: "activity", className: "h-4 w-4" }), t('moduleOverview.status')] }), _jsx("dd", { children: _jsx(Badge, { variant: statusVariant, className: "capitalize", children: module.status }) })] }), module.owners?.team && (_jsxs("div", { className: "flex items-center justify-between py-2 border-b border-border", children: [_jsxs("dt", { className: "text-sm font-medium text-muted-foreground flex items-center gap-2", children: [_jsx(Icon, { name: "users", className: "h-4 w-4" }), t('moduleOverview.owner')] }), _jsx("dd", { className: "text-sm font-medium", children: module.owners.team })] })), module.owners?.supportChannel && (_jsxs("div", { className: "flex items-center justify-between py-2", children: [_jsxs("dt", { className: "text-sm font-medium text-muted-foreground flex items-center gap-2", children: [_jsx(Icon, { name: "message-circle", className: "h-4 w-4" }), t('moduleOverview.support')] }), _jsx("dd", { className: "text-sm font-medium", children: module.owners.supportChannel })] }))] }) })] })] }));
40
24
  }
@@ -1,2 +1 @@
1
1
  export declare function ProfilePage(): import("react/jsx-runtime").JSX.Element;
2
- //# sourceMappingURL=ProfilePage.d.ts.map
@@ -26,5 +26,5 @@ export function ProfilePage() {
26
26
  const handlePinnedClick = (route) => {
27
27
  navigate(route);
28
28
  };
29
- return (_jsxs("div", { className: "p-6 max-w-3xl mx-auto space-y-6", "data-testid": "profile-page", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-bold mb-1", "data-testid": "profile-heading", children: t('profilePage.title') }), _jsx("p", { className: "text-muted-foreground", children: t('profilePage.userInfo') })] }), _jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "user", className: "h-5 w-5 text-muted-foreground" }), _jsx(CardTitle, { className: "text-lg", children: t('profilePage.userInfo') })] }) }), _jsxs(CardContent, { children: [_jsxs("div", { className: "flex items-center gap-4 mb-6", children: [_jsx(Avatar, { className: "h-16 w-16", children: _jsx(AvatarFallback, { className: "text-2xl font-bold bg-primary text-primary-foreground", children: user?.displayName?.charAt(0).toUpperCase() || "U" }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-xl font-semibold", children: user?.displayName || "User" }), _jsx("p", { className: "text-muted-foreground", children: user?.email || "No email" })] })] }), _jsxs("dl", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between py-2 border-b border-border", children: [_jsx("dt", { className: "text-sm font-medium text-muted-foreground", children: t('profilePage.name') }), _jsx("dd", { "data-testid": "profile-display-name", className: `text-sm font-medium ${!user?.displayName ? "text-muted-foreground" : ""}`, children: user?.displayName || "N/A" })] }), _jsxs("div", { className: "flex items-center justify-between py-2 border-b border-border", children: [_jsx("dt", { className: "text-sm font-medium text-muted-foreground", children: t('profilePage.email') }), _jsx("dd", { "data-testid": "profile-email", className: `text-sm font-medium ${!user?.email ? "text-muted-foreground" : ""}`, children: user?.email || "N/A" })] }), _jsxs("div", { className: "flex items-center justify-between py-2", children: [_jsx("dt", { className: "text-sm font-medium text-muted-foreground", children: t('profilePage.roles') }), _jsx("dd", { "data-testid": "profile-roles", children: _jsx(Badge, { variant: "secondary", children: "Admin" }) })] })] })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "pin", className: "h-5 w-5 text-muted-foreground" }), _jsx(CardTitle, { className: "text-lg", children: t('profilePage.pinnedPages') })] }), _jsx(CardDescription, { children: t('profilePage.pinnedPages') })] }), _jsx(CardContent, { children: pinnedItems.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground py-2", children: t('profilePage.noPinnedPages') })) : (_jsx("ul", { "data-testid": "pinned-pages-list", className: "space-y-1", children: pinnedItems.map((item) => (_jsx("li", { children: _jsxs("button", { "data-testid": `pinned-page-${item.commandId}`, onClick: () => handlePinnedClick(item.route), className: "w-full flex items-center gap-3 text-left px-3 py-2.5 rounded-md hover:bg-accent transition-colors", children: [_jsx(Icon, { name: item.commandIcon || item.moduleIcon || "file-text", className: "h-4 w-4 text-muted-foreground" }), _jsxs("span", { className: "text-sm", children: [item.moduleTitle, " ", _jsx("span", { className: "text-muted-foreground", children: "/" }), " ", item.commandTitle] })] }) }, `${item.moduleId}-${item.commandId}`))) })) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "shield", className: "h-5 w-5 text-muted-foreground" }), _jsx(CardTitle, { className: "text-lg", children: t('profilePage.auditInfo') })] }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between py-2", children: [_jsx("span", { className: "text-sm font-medium text-muted-foreground", children: t('profilePage.userId') }), _jsx("span", { "data-testid": "profile-user-id", className: `text-sm font-mono px-2 py-1 rounded ${user?.id ? "bg-muted" : "text-muted-foreground"}`, children: user?.id || "N/A" })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: t('profilePage.auditInfo') })] })] })] }));
29
+ return (_jsxs("div", { className: "p-6 max-w-3xl mx-auto space-y-6", "data-testid": "profile-page", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-bold mb-1", "data-testid": "profile-heading", children: t('profilePage.title') }), _jsx("p", { className: "text-muted-foreground", children: t('profilePage.userInfo') })] }), _jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "user", className: "h-5 w-5 text-muted-foreground" }), _jsx(CardTitle, { className: "text-lg", children: t('profilePage.userInfo') })] }) }), _jsxs(CardContent, { children: [_jsxs("div", { className: "flex items-center gap-4 mb-6", children: [_jsx(Avatar, { className: "h-16 w-16", children: _jsx(AvatarFallback, { className: "text-2xl font-bold bg-primary text-primary-foreground", children: user?.displayName?.charAt(0).toUpperCase() || "U" }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-xl font-semibold", children: user?.displayName || "User" }), _jsx("p", { className: "text-muted-foreground", children: user?.email || "No email" })] })] }), _jsxs("dl", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between py-2 border-b border-border", children: [_jsx("dt", { className: "text-sm font-medium text-muted-foreground", children: t('profilePage.name') }), _jsx("dd", { "data-testid": "profile-display-name", className: `text-sm font-medium ${!user?.displayName ? "text-muted-foreground" : ""}`, children: user?.displayName || "N/A" })] }), _jsxs("div", { className: "flex items-center justify-between py-2 border-b border-border", children: [_jsx("dt", { className: "text-sm font-medium text-muted-foreground", children: t('profilePage.email') }), _jsx("dd", { "data-testid": "profile-email", className: `text-sm font-medium ${!user?.email ? "text-muted-foreground" : ""}`, children: user?.email || "N/A" })] }), _jsxs("div", { className: "flex items-center justify-between py-2", children: [_jsx("dt", { className: "text-sm font-medium text-muted-foreground", children: t('profilePage.roles') }), _jsx("dd", { "data-testid": "profile-roles", className: "flex flex-wrap gap-1 px-2", children: user?.roles && user.roles.length > 0 ? (user.roles.map((role) => (_jsx(Badge, { variant: "secondary", children: role }, role)))) : (_jsx(Badge, { variant: "outline", children: "No roles" })) })] })] })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "pin", className: "h-5 w-5 text-muted-foreground" }), _jsx(CardTitle, { className: "text-lg", children: t('profilePage.pinnedPages') })] }), _jsx(CardDescription, { children: t('profilePage.pinnedPages') })] }), _jsx(CardContent, { children: pinnedItems.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground py-2", children: t('profilePage.noPinnedPages') })) : (_jsx("ul", { "data-testid": "pinned-pages-list", className: "space-y-1", children: pinnedItems.map((item) => (_jsx("li", { children: _jsxs("button", { "data-testid": `pinned-page-${item.commandId}`, onClick: () => handlePinnedClick(item.route), className: "w-full flex items-center gap-3 text-left px-3 py-2.5 rounded-md hover:bg-accent transition-colors", children: [_jsx(Icon, { name: item.commandIcon || item.moduleIcon || "file-text", className: "h-4 w-4 text-muted-foreground" }), _jsxs("span", { className: "text-sm", children: [item.moduleTitle, " ", _jsx("span", { className: "text-muted-foreground", children: "/" }), " ", item.commandTitle] })] }) }, `${item.moduleId}-${item.commandId}`))) })) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "shield", className: "h-5 w-5 text-muted-foreground" }), _jsx(CardTitle, { className: "text-lg", children: t('profilePage.auditInfo') })] }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between py-2", children: [_jsx("span", { className: "text-sm font-medium text-muted-foreground", children: t('profilePage.userId') }), _jsx("span", { "data-testid": "profile-user-id", className: `text-sm font-mono px-2 py-1 rounded ${user?.id ? "bg-muted" : "text-muted-foreground"}`, children: user?.id || "N/A" })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: t('profilePage.auditInfo') })] })] })] }));
30
30
  }
@@ -5,4 +5,3 @@ interface RegistryPageProps {
5
5
  }
6
6
  export declare function RegistryPage({ apiUrl, registryClient }: RegistryPageProps): import("react/jsx-runtime").JSX.Element;
7
7
  export {};
8
- //# sourceMappingURL=RegistryPage.d.ts.map