@nsxbet/admin-sdk 0.1.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 (159) hide show
  1. package/README.md +680 -0
  2. package/dist/auth/client/in-memory.d.ts +27 -0
  3. package/dist/auth/client/in-memory.d.ts.map +1 -0
  4. package/dist/auth/client/in-memory.js +242 -0
  5. package/dist/auth/client/index.d.ts +7 -0
  6. package/dist/auth/client/index.d.ts.map +1 -0
  7. package/dist/auth/client/index.js +7 -0
  8. package/dist/auth/client/interface.d.ts +115 -0
  9. package/dist/auth/client/interface.d.ts.map +1 -0
  10. package/dist/auth/client/interface.js +7 -0
  11. package/dist/auth/client/keycloak.d.ts +19 -0
  12. package/dist/auth/client/keycloak.d.ts.map +1 -0
  13. package/dist/auth/client/keycloak.js +126 -0
  14. package/dist/auth/components/UserSelector.d.ts +19 -0
  15. package/dist/auth/components/UserSelector.d.ts.map +1 -0
  16. package/dist/auth/components/UserSelector.js +100 -0
  17. package/dist/auth/components/index.d.ts +5 -0
  18. package/dist/auth/components/index.d.ts.map +1 -0
  19. package/dist/auth/components/index.js +4 -0
  20. package/dist/auth/index.d.ts +7 -0
  21. package/dist/auth/index.d.ts.map +1 -0
  22. package/dist/auth/index.js +7 -0
  23. package/dist/components/AuthProvider.d.ts +48 -0
  24. package/dist/components/AuthProvider.d.ts.map +1 -0
  25. package/dist/components/AuthProvider.js +117 -0
  26. package/dist/hooks/useAuth.d.ts +21 -0
  27. package/dist/hooks/useAuth.d.ts.map +1 -0
  28. package/dist/hooks/useAuth.js +34 -0
  29. package/dist/hooks/useFetch.d.ts +8 -0
  30. package/dist/hooks/useFetch.d.ts.map +1 -0
  31. package/dist/hooks/useFetch.js +31 -0
  32. package/dist/hooks/useI18n.d.ts +46 -0
  33. package/dist/hooks/useI18n.d.ts.map +1 -0
  34. package/dist/hooks/useI18n.js +95 -0
  35. package/dist/hooks/usePlatformAPI.d.ts +12 -0
  36. package/dist/hooks/usePlatformAPI.d.ts.map +1 -0
  37. package/dist/hooks/usePlatformAPI.js +10 -0
  38. package/dist/hooks/useTelemetry.d.ts +17 -0
  39. package/dist/hooks/useTelemetry.d.ts.map +1 -0
  40. package/dist/hooks/useTelemetry.js +36 -0
  41. package/dist/i18n/config.d.ts +26 -0
  42. package/dist/i18n/config.d.ts.map +1 -0
  43. package/dist/i18n/config.js +92 -0
  44. package/dist/i18n/index.d.ts +6 -0
  45. package/dist/i18n/index.d.ts.map +1 -0
  46. package/dist/i18n/index.js +4 -0
  47. package/dist/i18n/locales/en-US.json +144 -0
  48. package/dist/i18n/locales/es.json +144 -0
  49. package/dist/i18n/locales/pt-BR.json +144 -0
  50. package/dist/i18n/locales/ro.json +144 -0
  51. package/dist/index.d.ts +27 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +30 -0
  54. package/dist/registry/AdminShellRegistry.d.ts +140 -0
  55. package/dist/registry/AdminShellRegistry.d.ts.map +1 -0
  56. package/dist/registry/AdminShellRegistry.js +237 -0
  57. package/dist/registry/client/http.d.ts +21 -0
  58. package/dist/registry/client/http.d.ts.map +1 -0
  59. package/dist/registry/client/http.js +107 -0
  60. package/dist/registry/client/in-memory.d.ts +36 -0
  61. package/dist/registry/client/in-memory.d.ts.map +1 -0
  62. package/dist/registry/client/in-memory.js +242 -0
  63. package/dist/registry/client/index.d.ts +7 -0
  64. package/dist/registry/client/index.d.ts.map +1 -0
  65. package/dist/registry/client/index.js +5 -0
  66. package/dist/registry/client/interface.d.ts +96 -0
  67. package/dist/registry/client/interface.d.ts.map +1 -0
  68. package/dist/registry/client/interface.js +7 -0
  69. package/dist/registry/index.d.ts +12 -0
  70. package/dist/registry/index.d.ts.map +1 -0
  71. package/dist/registry/index.js +8 -0
  72. package/dist/registry/types/index.d.ts +9 -0
  73. package/dist/registry/types/index.d.ts.map +1 -0
  74. package/dist/registry/types/index.js +6 -0
  75. package/dist/registry/types/manifest.d.ts +98 -0
  76. package/dist/registry/types/manifest.d.ts.map +1 -0
  77. package/dist/registry/types/manifest.js +81 -0
  78. package/dist/registry/types/module.d.ts +115 -0
  79. package/dist/registry/types/module.d.ts.map +1 -0
  80. package/dist/registry/types/module.js +6 -0
  81. package/dist/router/DynamicModule.d.ts +50 -0
  82. package/dist/router/DynamicModule.d.ts.map +1 -0
  83. package/dist/router/DynamicModule.js +141 -0
  84. package/dist/router/index.d.ts +2 -0
  85. package/dist/router/index.d.ts.map +1 -0
  86. package/dist/router/index.js +1 -0
  87. package/dist/shell/AdminShell.d.ts +38 -0
  88. package/dist/shell/AdminShell.d.ts.map +1 -0
  89. package/dist/shell/AdminShell.js +299 -0
  90. package/dist/shell/BackofficeShell.d.ts +38 -0
  91. package/dist/shell/BackofficeShell.d.ts.map +1 -0
  92. package/dist/shell/BackofficeShell.js +299 -0
  93. package/dist/shell/components/CommandPalette.d.ts +8 -0
  94. package/dist/shell/components/CommandPalette.d.ts.map +1 -0
  95. package/dist/shell/components/CommandPalette.js +197 -0
  96. package/dist/shell/components/HomePage.d.ts +2 -0
  97. package/dist/shell/components/HomePage.d.ts.map +1 -0
  98. package/dist/shell/components/HomePage.js +32 -0
  99. package/dist/shell/components/LeftNav.d.ts +7 -0
  100. package/dist/shell/components/LeftNav.d.ts.map +1 -0
  101. package/dist/shell/components/LeftNav.js +247 -0
  102. package/dist/shell/components/MainContent.d.ts +9 -0
  103. package/dist/shell/components/MainContent.d.ts.map +1 -0
  104. package/dist/shell/components/MainContent.js +88 -0
  105. package/dist/shell/components/ModuleOverview.d.ts +7 -0
  106. package/dist/shell/components/ModuleOverview.d.ts.map +1 -0
  107. package/dist/shell/components/ModuleOverview.js +40 -0
  108. package/dist/shell/components/ProfilePage.d.ts +2 -0
  109. package/dist/shell/components/ProfilePage.d.ts.map +1 -0
  110. package/dist/shell/components/ProfilePage.js +30 -0
  111. package/dist/shell/components/RegistryPage.d.ts +8 -0
  112. package/dist/shell/components/RegistryPage.d.ts.map +1 -0
  113. package/dist/shell/components/RegistryPage.js +129 -0
  114. package/dist/shell/components/SettingsPage.d.ts +2 -0
  115. package/dist/shell/components/SettingsPage.d.ts.map +1 -0
  116. package/dist/shell/components/SettingsPage.js +60 -0
  117. package/dist/shell/components/TopBar.d.ts +8 -0
  118. package/dist/shell/components/TopBar.d.ts.map +1 -0
  119. package/dist/shell/components/TopBar.js +61 -0
  120. package/dist/shell/components/index.d.ts +10 -0
  121. package/dist/shell/components/index.d.ts.map +1 -0
  122. package/dist/shell/components/index.js +7 -0
  123. package/dist/shell/components/theme-provider.d.ts +15 -0
  124. package/dist/shell/components/theme-provider.d.ts.map +1 -0
  125. package/dist/shell/components/theme-provider.js +39 -0
  126. package/dist/shell/index.d.ts +9 -0
  127. package/dist/shell/index.d.ts.map +1 -0
  128. package/dist/shell/index.js +8 -0
  129. package/dist/shell/search/fuzzy.d.ts +18 -0
  130. package/dist/shell/search/fuzzy.d.ts.map +1 -0
  131. package/dist/shell/search/fuzzy.js +121 -0
  132. package/dist/shell/search/index.d.ts +3 -0
  133. package/dist/shell/search/index.d.ts.map +1 -0
  134. package/dist/shell/search/index.js +1 -0
  135. package/dist/shell/telemetry.d.ts +7 -0
  136. package/dist/shell/telemetry.d.ts.map +1 -0
  137. package/dist/shell/telemetry.js +25 -0
  138. package/dist/shell/types.d.ts +110 -0
  139. package/dist/shell/types.d.ts.map +1 -0
  140. package/dist/shell/types.js +4 -0
  141. package/dist/tailwind/index.d.ts +20 -0
  142. package/dist/tailwind/index.d.ts.map +1 -0
  143. package/dist/tailwind/index.js +42 -0
  144. package/dist/types/keycloak.d.ts +26 -0
  145. package/dist/types/keycloak.d.ts.map +1 -0
  146. package/dist/types/keycloak.js +1 -0
  147. package/dist/types/platform.d.ts +83 -0
  148. package/dist/types/platform.d.ts.map +1 -0
  149. package/dist/types/platform.js +5 -0
  150. package/dist/vite/config.d.ts +71 -0
  151. package/dist/vite/config.d.ts.map +1 -0
  152. package/dist/vite/config.js +87 -0
  153. package/dist/vite/index.d.ts +18 -0
  154. package/dist/vite/index.d.ts.map +1 -0
  155. package/dist/vite/index.js +17 -0
  156. package/dist/vite/plugins.d.ts +44 -0
  157. package/dist/vite/plugins.d.ts.map +1 -0
  158. package/dist/vite/plugins.js +74 -0
  159. package/package.json +86 -0
@@ -0,0 +1,197 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState, useCallback } from "react";
3
+ import { useNavigate } from "react-router-dom";
4
+ import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Icon, } from "@nsxbet/admin-ui";
5
+ import { fuzzySearch } from "../search/fuzzy";
6
+ import { useAuth } from "../../hooks/useAuth";
7
+ import { useI18n } from "../../hooks/useI18n";
8
+ // Storage key for pinned commands
9
+ const PINNED_COMMANDS_KEY = "adminPlatform.pinnedCommands";
10
+ function parseSearch(search) {
11
+ const trimmed = search.trim();
12
+ // @term - filter modules only
13
+ if (trimmed.startsWith("@")) {
14
+ return { filter: "module", query: trimmed.slice(1).trim() };
15
+ }
16
+ // >term - filter commands only
17
+ if (trimmed.startsWith(">")) {
18
+ return { filter: "command", query: trimmed.slice(1).trim() };
19
+ }
20
+ // #category - filter by category
21
+ if (trimmed.startsWith("#")) {
22
+ return { filter: "category", query: "", categoryFilter: trimmed.slice(1).trim().toLowerCase() };
23
+ }
24
+ return { filter: "all", query: trimmed };
25
+ }
26
+ export function CommandPalette({ open, onOpenChange, catalog, }) {
27
+ const navigate = useNavigate();
28
+ const { hasPermission } = useAuth();
29
+ const { t, i18n } = useI18n();
30
+ const [search, setSearch] = useState("");
31
+ const [pinnedCommands, setPinnedCommands] = useState([]);
32
+ // Helper to translate module/command titles
33
+ // titleKey format: "namespace:key" (e.g., "tasks:module.title")
34
+ const translateTitle = useCallback((title, titleKey) => {
35
+ if (titleKey && titleKey.includes(':')) {
36
+ const [ns, key] = titleKey.split(':');
37
+ const translated = i18n.t(key, { ns, defaultValue: title });
38
+ return translated;
39
+ }
40
+ if (titleKey) {
41
+ const translated = t(titleKey, { defaultValue: title });
42
+ return translated;
43
+ }
44
+ return title;
45
+ }, [i18n, t]);
46
+ // Track dialog open count to reset key and force re-render
47
+ const [openCount, setOpenCount] = useState(0);
48
+ // Load pinned commands from localStorage
49
+ useEffect(() => {
50
+ try {
51
+ const stored = localStorage.getItem(PINNED_COMMANDS_KEY);
52
+ if (stored) {
53
+ setPinnedCommands(JSON.parse(stored));
54
+ }
55
+ }
56
+ catch {
57
+ // Ignore parse errors
58
+ }
59
+ }, [open]); // Reload when dialog opens
60
+ // Reset search when dialog closes
61
+ useEffect(() => {
62
+ if (!open) {
63
+ setSearch("");
64
+ }
65
+ else {
66
+ // Increment count when dialog opens to reset cmdk internal state
67
+ setOpenCount((c) => c + 1);
68
+ }
69
+ }, [open]);
70
+ // Parse search query for filters
71
+ const parsedSearch = useMemo(() => parseSearch(search), [search]);
72
+ // Build search index from catalog
73
+ const searchableItems = useMemo(() => {
74
+ if (!catalog)
75
+ return [];
76
+ const searchIndex = [];
77
+ // Add modules and commands (filtered by permissions)
78
+ for (const module of catalog.modules) {
79
+ // Check if user has permission to view this module
80
+ const viewPermissions = module.permissions?.view || [];
81
+ const hasAccess = viewPermissions.length === 0 ||
82
+ viewPermissions.some((perm) => hasPermission(perm));
83
+ if (!hasAccess)
84
+ continue;
85
+ // Get translated titles for search
86
+ const translatedModuleTitle = translateTitle(module.title, module.titleKey);
87
+ // Build keywords including translated title
88
+ const moduleKeywords = [
89
+ ...(module.keywords || []),
90
+ // Add translated title as keyword for search in current language
91
+ translatedModuleTitle !== module.title ? translatedModuleTitle : null,
92
+ ].filter((k) => k !== null);
93
+ // Add module itself to search index
94
+ searchIndex.push({
95
+ type: "module",
96
+ id: module.id,
97
+ title: module.title,
98
+ titleKey: module.titleKey,
99
+ category: module.category,
100
+ keywords: moduleKeywords,
101
+ description: module.description,
102
+ route: module.routeBase,
103
+ moduleId: module.id,
104
+ moduleTitle: module.title,
105
+ moduleTitleKey: module.titleKey,
106
+ icon: module.icon,
107
+ });
108
+ // Add commands from module with module title as context
109
+ if (module.commands) {
110
+ for (const command of module.commands) {
111
+ // Get translated command title
112
+ const translatedCommandTitle = translateTitle(command.title, command.titleKey);
113
+ // Build keywords including translated titles
114
+ const commandKeywords = [
115
+ ...(command.keywords || []),
116
+ module.title,
117
+ `@${module.title}`,
118
+ // Add translated titles as keywords for search in current language
119
+ translatedCommandTitle !== command.title ? translatedCommandTitle : null,
120
+ translatedModuleTitle !== module.title ? translatedModuleTitle : null,
121
+ translatedModuleTitle !== module.title ? `@${translatedModuleTitle}` : null,
122
+ ].filter((k) => k !== null);
123
+ searchIndex.push({
124
+ type: "command",
125
+ id: `${module.id}:${command.id}`,
126
+ title: command.title,
127
+ titleKey: command.titleKey,
128
+ keywords: commandKeywords,
129
+ route: command.route,
130
+ moduleId: module.id,
131
+ moduleTitle: module.title,
132
+ moduleTitleKey: module.titleKey,
133
+ category: module.category,
134
+ icon: command.icon,
135
+ });
136
+ }
137
+ }
138
+ }
139
+ return searchIndex;
140
+ }, [catalog, hasPermission, translateTitle]);
141
+ // Perform fuzzy search with filters
142
+ const searchResults = useMemo(() => {
143
+ let items = searchableItems;
144
+ // Apply type filter
145
+ if (parsedSearch.filter === "module") {
146
+ items = items.filter((item) => item.type === "module");
147
+ }
148
+ else if (parsedSearch.filter === "command") {
149
+ items = items.filter((item) => item.type === "command");
150
+ }
151
+ else if (parsedSearch.filter === "category" && parsedSearch.categoryFilter) {
152
+ items = items.filter((item) => item.category?.toLowerCase().includes(parsedSearch.categoryFilter) ?? false);
153
+ }
154
+ return fuzzySearch(items, parsedSearch.query, { maxResults: 50 });
155
+ }, [searchableItems, parsedSearch]);
156
+ // Get pinned items that exist in searchable items
157
+ const pinnedItems = useMemo(() => {
158
+ if (parsedSearch.query || parsedSearch.filter !== "all")
159
+ return []; // Hide when searching or filtering
160
+ return pinnedCommands
161
+ .map((pinned) => {
162
+ const item = searchableItems.find((s) => s.type === "command" && s.id === `${pinned.moduleId}:${pinned.commandId}`);
163
+ if (item) {
164
+ return { item, score: 1, matchedFields: [] };
165
+ }
166
+ // Fallback for pinned items that might not be in catalog yet
167
+ return {
168
+ item: {
169
+ type: "command",
170
+ id: `${pinned.moduleId}:${pinned.commandId}`,
171
+ title: pinned.title,
172
+ route: pinned.route,
173
+ moduleId: pinned.moduleId,
174
+ moduleTitle: pinned.moduleTitle,
175
+ icon: pinned.icon,
176
+ },
177
+ score: 1,
178
+ matchedFields: [],
179
+ };
180
+ })
181
+ .filter((r) => r !== null);
182
+ }, [pinnedCommands, searchableItems, parsedSearch]);
183
+ // Handle selection
184
+ const handleSelect = useCallback((route) => {
185
+ navigate(route);
186
+ onOpenChange(false);
187
+ }, [navigate, onOpenChange]);
188
+ // Group results by type
189
+ const moduleResults = searchResults.filter((r) => r.item.type === "module");
190
+ const commandResults = searchResults.filter((r) => r.item.type === "command");
191
+ // Check if a command is pinned
192
+ const isPinned = useCallback((itemId) => {
193
+ const [moduleId, commandId] = itemId.split(":");
194
+ return pinnedCommands.some((p) => p.moduleId === moduleId && p.commandId === commandId);
195
+ }, [pinnedCommands]);
196
+ return (_jsxs(CommandDialog, { open: open, onOpenChange: onOpenChange, "data-testid": "command-palette", children: [_jsx(CommandInput, { placeholder: t('commandPalette.placeholder'), value: search, onValueChange: setSearch, "data-testid": "command-palette-input", autoFocus: true }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: _jsxs("div", { className: "flex flex-col items-center justify-center py-6 text-center", children: [_jsx(Icon, { name: "search-x", className: "h-10 w-10 text-muted-foreground/50 mb-2" }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('commandPalette.noResults') }), _jsx("p", { className: "text-xs text-muted-foreground/70 mt-1", children: "@module, >command, #category" })] }) }), pinnedItems.length > 0 && (_jsx(CommandGroup, { heading: t('nav.pinned'), children: pinnedItems.map((result) => (_jsxs(CommandItem, { onSelect: () => handleSelect(result.item.route), "data-testid": "search-result-pinned", className: "flex items-center gap-2", children: [_jsx(Icon, { name: "pin", className: "h-4 w-4 text-primary shrink-0" }), _jsx(Icon, { name: result.item.icon || "terminal", className: "h-4 w-4 text-muted-foreground shrink-0" }), _jsx("span", { className: "flex-1 truncate", children: translateTitle(result.item.title, result.item.titleKey) }), _jsxs("span", { className: "text-xs text-muted-foreground/60 shrink-0", children: ["@", translateTitle(result.item.moduleTitle || '', result.item.moduleTitleKey)] }), _jsx("kbd", { className: "pointer-events-none hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100 sm:flex", children: "\u21B5" })] }, `pinned-${result.item.id}`))) })), moduleResults.length > 0 && (_jsx(CommandGroup, { heading: t('commandPalette.modules'), children: moduleResults.map((result) => (_jsxs(CommandItem, { onSelect: () => handleSelect(result.item.route), "data-testid": "search-result", className: "flex items-center gap-2", children: [_jsx(Icon, { name: result.item.icon || "folder", className: "h-4 w-4 text-muted-foreground shrink-0" }), _jsx("span", { className: "flex-1 truncate", children: translateTitle(result.item.title, result.item.titleKey) }), result.item.category && (_jsx("span", { className: "text-xs text-muted-foreground/60 shrink-0", children: result.item.category })), _jsx("kbd", { className: "pointer-events-none hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100 sm:flex", children: "\u21B5" })] }, result.item.id))) })), commandResults.length > 0 && (_jsx(CommandGroup, { heading: t('commandPalette.commands'), children: commandResults.map((result) => (_jsxs(CommandItem, { onSelect: () => handleSelect(result.item.route), "data-testid": "search-result", className: "flex items-center gap-2", children: [isPinned(result.item.id) && (_jsx(Icon, { name: "pin", className: "h-3 w-3 text-primary shrink-0" })), _jsx(Icon, { name: result.item.icon || "terminal", className: "h-4 w-4 text-muted-foreground shrink-0" }), _jsx("span", { className: "flex-1 truncate", children: translateTitle(result.item.title, result.item.titleKey) }), _jsxs("span", { className: "text-xs text-muted-foreground/60 shrink-0", children: ["@", translateTitle(result.item.moduleTitle || '', result.item.moduleTitleKey)] }), _jsx("kbd", { className: "pointer-events-none hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100 sm:flex", children: "\u21B5" })] }, result.item.id))) }))] })] }, `command-palette-${openCount}`));
197
+ }
@@ -0,0 +1,2 @@
1
+ export declare function HomePage(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=HomePage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HomePage.d.ts","sourceRoot":"","sources":["../../../src/shell/components/HomePage.tsx"],"names":[],"mappings":"AAoCA,wBAAgB,QAAQ,4CA+CvB"}
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useNavigate } from "react-router-dom";
3
+ import { Icon, Card, CardContent } from "@nsxbet/admin-ui";
4
+ import { useI18n } from "../../hooks/useI18n";
5
+ const quickActions = [
6
+ {
7
+ icon: "settings",
8
+ titleKey: "nav.settings",
9
+ descriptionKey: "settingsPage.appearanceDescription",
10
+ route: "/_settings",
11
+ color: "text-blue-500",
12
+ },
13
+ {
14
+ icon: "user",
15
+ titleKey: "nav.profile",
16
+ descriptionKey: "profilePage.userInfo",
17
+ route: "/_profile",
18
+ color: "text-green-500",
19
+ },
20
+ {
21
+ icon: "package",
22
+ titleKey: "nav.registry",
23
+ descriptionKey: "registryPage.description",
24
+ route: "/_registry",
25
+ color: "text-purple-500",
26
+ },
27
+ ];
28
+ export function HomePage() {
29
+ const navigate = useNavigate();
30
+ const { t } = useI18n();
31
+ return (_jsx("div", { className: "flex h-full items-center justify-center p-6", children: _jsxs("div", { className: "text-center max-w-2xl w-full", children: [_jsxs("div", { className: "mb-8", children: [_jsx("div", { className: "inline-flex p-4 rounded-full bg-primary/10 mb-4", children: _jsx(Icon, { name: "layout-dashboard", className: "h-12 w-12 text-primary" }) }), _jsx("h1", { className: "text-3xl font-bold mb-2", children: t('homePage.welcome') }), _jsx("p", { className: "text-muted-foreground", children: t('homePage.description') })] }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8", children: quickActions.map((action) => (_jsx(Card, { className: "cursor-pointer transition-all hover:shadow-md hover:border-primary/50 group", onClick: () => navigate(action.route), children: _jsxs(CardContent, { className: "p-4 flex flex-col items-center text-center", children: [_jsx("div", { className: `p-3 rounded-full bg-muted mb-3 group-hover:bg-primary/10 transition-colors`, children: _jsx(Icon, { name: action.icon, className: `h-6 w-6 ${action.color} group-hover:text-primary transition-colors` }) }), _jsx("h3", { className: "font-semibold text-sm mb-1", children: t(action.titleKey) }), _jsx("p", { className: "text-xs text-muted-foreground", children: t(action.descriptionKey) })] }) }, action.route))) }), _jsxs("div", { className: "flex items-center justify-center gap-2 text-sm text-muted-foreground", children: [_jsx(Icon, { name: "keyboard", className: "h-4 w-4" }), _jsx("span", { children: t('commandPalette.hint') })] })] }) }));
32
+ }
@@ -0,0 +1,7 @@
1
+ import type { Module } from "../types";
2
+ interface LeftNavProps {
3
+ modules: Module[];
4
+ }
5
+ export declare function LeftNav({ modules }: LeftNavProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
7
+ //# sourceMappingURL=LeftNav.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LeftNav.d.ts","sourceRoot":"","sources":["../../../src/shell/components/LeftNav.tsx"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAmBvC,UAAU,YAAY;IACpB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,wBAAgB,OAAO,CAAC,EAAE,OAAO,EAAE,EAAE,YAAY,2CAqiBhD"}
@@ -0,0 +1,247 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect, useMemo, useCallback } from "react";
3
+ import { useNavigate, useLocation } from "react-router-dom";
4
+ import { useAuth } from "../../hooks/useAuth";
5
+ import { useAuthContext } from "../../components/AuthProvider";
6
+ import { useI18n } from "../../hooks/useI18n";
7
+ 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";
8
+ export function LeftNav({ modules }) {
9
+ const navigate = useNavigate();
10
+ const location = useLocation();
11
+ const auth = useAuth();
12
+ const { user, logout } = useAuthContext();
13
+ const { state } = useSidebar();
14
+ const { t, i18n } = useI18n();
15
+ const isCollapsed = state === "collapsed";
16
+ const [showPinned, setShowPinned] = useState(true);
17
+ const [pinnedCommands, setPinnedCommands] = useState([]);
18
+ // Load preferences from localStorage
19
+ useEffect(() => {
20
+ try {
21
+ const pinnedPref = localStorage.getItem("adminPlatform.showPinned");
22
+ if (pinnedPref !== null) {
23
+ setShowPinned(JSON.parse(pinnedPref));
24
+ }
25
+ }
26
+ catch {
27
+ // Ignore localStorage errors
28
+ }
29
+ }, []);
30
+ // Load pinned commands from localStorage
31
+ useEffect(() => {
32
+ const loadPinnedCommands = () => {
33
+ try {
34
+ const pinned = localStorage.getItem("adminPlatform.pinnedCommands");
35
+ if (pinned) {
36
+ setPinnedCommands(JSON.parse(pinned));
37
+ }
38
+ else {
39
+ setPinnedCommands([]);
40
+ }
41
+ }
42
+ catch {
43
+ // Ignore localStorage errors
44
+ }
45
+ };
46
+ // Initial load
47
+ loadPinnedCommands();
48
+ // Listen for storage events
49
+ const handleStorageChange = (e) => {
50
+ if (e.key === "adminPlatform.pinnedCommands") {
51
+ loadPinnedCommands();
52
+ }
53
+ else if (e.key === "adminPlatform.showPinned" && e.newValue) {
54
+ try {
55
+ setShowPinned(JSON.parse(e.newValue));
56
+ }
57
+ catch {
58
+ // Ignore parse errors
59
+ }
60
+ }
61
+ };
62
+ window.addEventListener("storage", handleStorageChange);
63
+ return () => window.removeEventListener("storage", handleStorageChange);
64
+ }, []);
65
+ const handleModuleClick = (module) => {
66
+ // Navigate to the shell-rendered overview page
67
+ navigate(`/_modules/${module.id}`);
68
+ };
69
+ // Helper to check if user has permission to view a module
70
+ const checkModulePermission = (module) => {
71
+ if (module.permissions.view.length === 0)
72
+ return true;
73
+ return module.permissions.view.some((perm) => auth.hasPermission(perm));
74
+ };
75
+ // Group modules by category (show all active modules, including those without permission)
76
+ const modulesByCategory = useMemo(() => {
77
+ const grouped = modules
78
+ .filter((m) => m.status === "active")
79
+ .map((module) => ({
80
+ ...module,
81
+ hasViewPermission: checkModulePermission(module),
82
+ }))
83
+ .reduce((acc, module) => {
84
+ const category = module.category;
85
+ if (!acc[category]) {
86
+ acc[category] = [];
87
+ }
88
+ acc[category].push(module);
89
+ return acc;
90
+ }, {});
91
+ // Sort modules within each category by navOrder then title
92
+ Object.keys(grouped).forEach((category) => {
93
+ grouped[category].sort((a, b) => {
94
+ if (a.navOrder !== undefined && b.navOrder !== undefined) {
95
+ return a.navOrder - b.navOrder;
96
+ }
97
+ if (a.navOrder !== undefined)
98
+ return -1;
99
+ if (b.navOrder !== undefined)
100
+ return 1;
101
+ return a.title.localeCompare(b.title);
102
+ });
103
+ });
104
+ return grouped;
105
+ }, [modules, auth]);
106
+ // Get sorted category names
107
+ const categories = Object.keys(modulesByCategory).sort();
108
+ // Permission denied tooltip message
109
+ const noPermissionTooltip = t('errors.accessDeniedDescription');
110
+ // Check if a route is active
111
+ const isRouteActive = (route) => {
112
+ return location.pathname === route || location.pathname.startsWith(route + "/");
113
+ };
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
+ const isCommandPinned = useCallback((moduleId, commandId) => {
133
+ return pinnedCommands.some((p) => p.moduleId === moduleId && p.commandId === commandId);
134
+ }, [pinnedCommands]);
135
+ // Toggle pin state for a command
136
+ const togglePin = useCallback((module, command) => {
137
+ const isPinned = isCommandPinned(module.id, command.id);
138
+ let updated;
139
+ if (isPinned) {
140
+ // Unpin
141
+ updated = pinnedCommands.filter((p) => !(p.moduleId === module.id && p.commandId === command.id));
142
+ }
143
+ else {
144
+ // Pin
145
+ const pinnedCommand = {
146
+ moduleId: module.id,
147
+ moduleTitle: module.title,
148
+ moduleTitleKey: module.titleKey,
149
+ moduleIcon: module.icon,
150
+ commandId: command.id,
151
+ commandTitle: command.title,
152
+ commandTitleKey: command.titleKey,
153
+ commandIcon: command.icon,
154
+ route: command.route,
155
+ };
156
+ updated = [...pinnedCommands, pinnedCommand];
157
+ }
158
+ setPinnedCommands(updated);
159
+ localStorage.setItem("adminPlatform.pinnedCommands", JSON.stringify(updated));
160
+ // Notify other components by dispatching a storage event
161
+ window.dispatchEvent(new StorageEvent("storage", {
162
+ key: "adminPlatform.pinnedCommands",
163
+ newValue: JSON.stringify(updated),
164
+ }));
165
+ }, [pinnedCommands, isCommandPinned]);
166
+ // Unpin a command by moduleId and commandId
167
+ const unpinCommand = useCallback((moduleId, commandId) => {
168
+ const updated = pinnedCommands.filter((p) => !(p.moduleId === moduleId && p.commandId === commandId));
169
+ setPinnedCommands(updated);
170
+ localStorage.setItem("adminPlatform.pinnedCommands", JSON.stringify(updated));
171
+ // Notify other components by dispatching a storage event
172
+ window.dispatchEvent(new StorageEvent("storage", {
173
+ key: "adminPlatform.pinnedCommands",
174
+ newValue: JSON.stringify(updated),
175
+ }));
176
+ }, [pinnedCommands]);
177
+ // Move a pinned item up in the list
178
+ const movePinnedUp = useCallback((index) => {
179
+ if (index <= 0)
180
+ return;
181
+ const updated = [...pinnedCommands];
182
+ [updated[index - 1], updated[index]] = [updated[index], updated[index - 1]];
183
+ setPinnedCommands(updated);
184
+ localStorage.setItem("adminPlatform.pinnedCommands", JSON.stringify(updated));
185
+ window.dispatchEvent(new StorageEvent("storage", {
186
+ key: "adminPlatform.pinnedCommands",
187
+ newValue: JSON.stringify(updated),
188
+ }));
189
+ }, [pinnedCommands]);
190
+ // Move a pinned item down in the list
191
+ const movePinnedDown = useCallback((index) => {
192
+ if (index >= pinnedCommands.length - 1)
193
+ return;
194
+ const updated = [...pinnedCommands];
195
+ [updated[index], updated[index + 1]] = [updated[index + 1], updated[index]];
196
+ setPinnedCommands(updated);
197
+ localStorage.setItem("adminPlatform.pinnedCommands", JSON.stringify(updated));
198
+ window.dispatchEvent(new StorageEvent("storage", {
199
+ key: "adminPlatform.pinnedCommands",
200
+ newValue: JSON.stringify(updated),
201
+ }));
202
+ }, [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')] })] })] }) }) }) })] }));
247
+ }
@@ -0,0 +1,9 @@
1
+ import { type ReactNode } from "react";
2
+ import type { Module } from "../types";
3
+ interface MainContentProps {
4
+ modules: Module[];
5
+ children?: ReactNode;
6
+ }
7
+ export declare function MainContent({ modules, children }: MainContentProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=MainContent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MainContent.d.ts","sourceRoot":"","sources":["../../../src/shell/components/MainContent.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA8C,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGnF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAOvC,UAAU,gBAAgB;IACxB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,WAAW,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,gBAAgB,2CAmIlE"}
@@ -0,0 +1,88 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment, useState, useEffect, useCallback } from "react";
3
+ import { useLocation, useNavigate } from "react-router-dom";
4
+ import { useI18n } from "../../hooks/useI18n";
5
+ export function MainContent({ modules, children }) {
6
+ const location = useLocation();
7
+ const navigate = useNavigate();
8
+ const { t, i18n } = useI18n();
9
+ 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
+ // Auto-detect module and update breadcrumbs based on current route
28
+ useEffect(() => {
29
+ const currentPath = location.pathname;
30
+ // Check if we're on settings page
31
+ if (currentPath === "/_settings") {
32
+ setBreadcrumbs([{ label: t('breadcrumbs.home'), href: "/" }, { label: t('breadcrumbs.settings') }]);
33
+ return;
34
+ }
35
+ // Check if we're on profile page
36
+ if (currentPath === "/_profile") {
37
+ setBreadcrumbs([{ label: t('breadcrumbs.home'), href: "/" }, { label: t('breadcrumbs.profile') }]);
38
+ return;
39
+ }
40
+ // Check if we're on registry page
41
+ if (currentPath === "/_registry") {
42
+ setBreadcrumbs([{ label: t('breadcrumbs.home'), href: "/" }, { label: t('breadcrumbs.registry') }]);
43
+ return;
44
+ }
45
+ // Find module that matches current path (for module routes)
46
+ const module = modules.find((m) => m.status === "active" &&
47
+ (currentPath === m.routeBase ||
48
+ currentPath.startsWith(m.routeBase + "/")));
49
+ if (module) {
50
+ // Check if current path matches a specific command
51
+ const command = module.commands?.find((cmd) => currentPath === cmd.route || currentPath.startsWith(cmd.route));
52
+ // Generate breadcrumbs from path
53
+ const crumbs = [{ label: t('breadcrumbs.home'), href: "/" }];
54
+ // Add module as breadcrumb (with translation)
55
+ const moduleHref = module.commands && module.commands.length > 0
56
+ ? module.commands[0].route
57
+ : module.routeBase;
58
+ crumbs.push({ label: translateTitle(module.title, module.titleKey), href: moduleHref });
59
+ if (command) {
60
+ // Add command as breadcrumb (with translation)
61
+ crumbs.push({ label: translateTitle(command.title, command.titleKey) });
62
+ }
63
+ else {
64
+ // Add sub-paths if any (for non-command routes)
65
+ const subPath = currentPath.replace(module.routeBase, "");
66
+ if (subPath && subPath !== "/") {
67
+ const parts = subPath.split("/").filter(Boolean);
68
+ parts.forEach((part, index) => {
69
+ const label = part.charAt(0).toUpperCase() + part.slice(1);
70
+ const isLast = index === parts.length - 1;
71
+ crumbs.push({
72
+ label,
73
+ href: isLast
74
+ ? undefined
75
+ : `${module.routeBase}/${parts.slice(0, index + 1).join("/")}`,
76
+ });
77
+ });
78
+ }
79
+ }
80
+ setBreadcrumbs(crumbs);
81
+ }
82
+ else {
83
+ // No module matched, show home
84
+ setBreadcrumbs([{ label: t('breadcrumbs.home') }]);
85
+ }
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 })] }));
88
+ }
@@ -0,0 +1,7 @@
1
+ import type { Module } from "../types";
2
+ interface ModuleOverviewProps {
3
+ modules: Module[];
4
+ }
5
+ export declare function ModuleOverview({ modules }: ModuleOverviewProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
7
+ //# sourceMappingURL=ModuleOverview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModuleOverview.d.ts","sourceRoot":"","sources":["../../../src/shell/components/ModuleOverview.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAEvC,UAAU,mBAAmB;IAC3B,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,wBAAgB,cAAc,CAAC,EAAE,OAAO,EAAE,EAAE,mBAAmB,2CAkK9D"}