@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.
- package/README.md +680 -0
- package/dist/auth/client/in-memory.d.ts +27 -0
- package/dist/auth/client/in-memory.d.ts.map +1 -0
- package/dist/auth/client/in-memory.js +242 -0
- package/dist/auth/client/index.d.ts +7 -0
- package/dist/auth/client/index.d.ts.map +1 -0
- package/dist/auth/client/index.js +7 -0
- package/dist/auth/client/interface.d.ts +115 -0
- package/dist/auth/client/interface.d.ts.map +1 -0
- package/dist/auth/client/interface.js +7 -0
- package/dist/auth/client/keycloak.d.ts +19 -0
- package/dist/auth/client/keycloak.d.ts.map +1 -0
- package/dist/auth/client/keycloak.js +126 -0
- package/dist/auth/components/UserSelector.d.ts +19 -0
- package/dist/auth/components/UserSelector.d.ts.map +1 -0
- package/dist/auth/components/UserSelector.js +100 -0
- package/dist/auth/components/index.d.ts +5 -0
- package/dist/auth/components/index.d.ts.map +1 -0
- package/dist/auth/components/index.js +4 -0
- package/dist/auth/index.d.ts +7 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +7 -0
- package/dist/components/AuthProvider.d.ts +48 -0
- package/dist/components/AuthProvider.d.ts.map +1 -0
- package/dist/components/AuthProvider.js +117 -0
- package/dist/hooks/useAuth.d.ts +21 -0
- package/dist/hooks/useAuth.d.ts.map +1 -0
- package/dist/hooks/useAuth.js +34 -0
- package/dist/hooks/useFetch.d.ts +8 -0
- package/dist/hooks/useFetch.d.ts.map +1 -0
- package/dist/hooks/useFetch.js +31 -0
- package/dist/hooks/useI18n.d.ts +46 -0
- package/dist/hooks/useI18n.d.ts.map +1 -0
- package/dist/hooks/useI18n.js +95 -0
- package/dist/hooks/usePlatformAPI.d.ts +12 -0
- package/dist/hooks/usePlatformAPI.d.ts.map +1 -0
- package/dist/hooks/usePlatformAPI.js +10 -0
- package/dist/hooks/useTelemetry.d.ts +17 -0
- package/dist/hooks/useTelemetry.d.ts.map +1 -0
- package/dist/hooks/useTelemetry.js +36 -0
- package/dist/i18n/config.d.ts +26 -0
- package/dist/i18n/config.d.ts.map +1 -0
- package/dist/i18n/config.js +92 -0
- package/dist/i18n/index.d.ts +6 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +4 -0
- package/dist/i18n/locales/en-US.json +144 -0
- package/dist/i18n/locales/es.json +144 -0
- package/dist/i18n/locales/pt-BR.json +144 -0
- package/dist/i18n/locales/ro.json +144 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/registry/AdminShellRegistry.d.ts +140 -0
- package/dist/registry/AdminShellRegistry.d.ts.map +1 -0
- package/dist/registry/AdminShellRegistry.js +237 -0
- package/dist/registry/client/http.d.ts +21 -0
- package/dist/registry/client/http.d.ts.map +1 -0
- package/dist/registry/client/http.js +107 -0
- package/dist/registry/client/in-memory.d.ts +36 -0
- package/dist/registry/client/in-memory.d.ts.map +1 -0
- package/dist/registry/client/in-memory.js +242 -0
- package/dist/registry/client/index.d.ts +7 -0
- package/dist/registry/client/index.d.ts.map +1 -0
- package/dist/registry/client/index.js +5 -0
- package/dist/registry/client/interface.d.ts +96 -0
- package/dist/registry/client/interface.d.ts.map +1 -0
- package/dist/registry/client/interface.js +7 -0
- package/dist/registry/index.d.ts +12 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +8 -0
- package/dist/registry/types/index.d.ts +9 -0
- package/dist/registry/types/index.d.ts.map +1 -0
- package/dist/registry/types/index.js +6 -0
- package/dist/registry/types/manifest.d.ts +98 -0
- package/dist/registry/types/manifest.d.ts.map +1 -0
- package/dist/registry/types/manifest.js +81 -0
- package/dist/registry/types/module.d.ts +115 -0
- package/dist/registry/types/module.d.ts.map +1 -0
- package/dist/registry/types/module.js +6 -0
- package/dist/router/DynamicModule.d.ts +50 -0
- package/dist/router/DynamicModule.d.ts.map +1 -0
- package/dist/router/DynamicModule.js +141 -0
- package/dist/router/index.d.ts +2 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +1 -0
- package/dist/shell/AdminShell.d.ts +38 -0
- package/dist/shell/AdminShell.d.ts.map +1 -0
- package/dist/shell/AdminShell.js +299 -0
- package/dist/shell/BackofficeShell.d.ts +38 -0
- package/dist/shell/BackofficeShell.d.ts.map +1 -0
- package/dist/shell/BackofficeShell.js +299 -0
- package/dist/shell/components/CommandPalette.d.ts +8 -0
- package/dist/shell/components/CommandPalette.d.ts.map +1 -0
- package/dist/shell/components/CommandPalette.js +197 -0
- package/dist/shell/components/HomePage.d.ts +2 -0
- package/dist/shell/components/HomePage.d.ts.map +1 -0
- package/dist/shell/components/HomePage.js +32 -0
- package/dist/shell/components/LeftNav.d.ts +7 -0
- package/dist/shell/components/LeftNav.d.ts.map +1 -0
- package/dist/shell/components/LeftNav.js +247 -0
- package/dist/shell/components/MainContent.d.ts +9 -0
- package/dist/shell/components/MainContent.d.ts.map +1 -0
- package/dist/shell/components/MainContent.js +88 -0
- package/dist/shell/components/ModuleOverview.d.ts +7 -0
- package/dist/shell/components/ModuleOverview.d.ts.map +1 -0
- package/dist/shell/components/ModuleOverview.js +40 -0
- package/dist/shell/components/ProfilePage.d.ts +2 -0
- package/dist/shell/components/ProfilePage.d.ts.map +1 -0
- package/dist/shell/components/ProfilePage.js +30 -0
- package/dist/shell/components/RegistryPage.d.ts +8 -0
- package/dist/shell/components/RegistryPage.d.ts.map +1 -0
- package/dist/shell/components/RegistryPage.js +129 -0
- package/dist/shell/components/SettingsPage.d.ts +2 -0
- package/dist/shell/components/SettingsPage.d.ts.map +1 -0
- package/dist/shell/components/SettingsPage.js +60 -0
- package/dist/shell/components/TopBar.d.ts +8 -0
- package/dist/shell/components/TopBar.d.ts.map +1 -0
- package/dist/shell/components/TopBar.js +61 -0
- package/dist/shell/components/index.d.ts +10 -0
- package/dist/shell/components/index.d.ts.map +1 -0
- package/dist/shell/components/index.js +7 -0
- package/dist/shell/components/theme-provider.d.ts +15 -0
- package/dist/shell/components/theme-provider.d.ts.map +1 -0
- package/dist/shell/components/theme-provider.js +39 -0
- package/dist/shell/index.d.ts +9 -0
- package/dist/shell/index.d.ts.map +1 -0
- package/dist/shell/index.js +8 -0
- package/dist/shell/search/fuzzy.d.ts +18 -0
- package/dist/shell/search/fuzzy.d.ts.map +1 -0
- package/dist/shell/search/fuzzy.js +121 -0
- package/dist/shell/search/index.d.ts +3 -0
- package/dist/shell/search/index.d.ts.map +1 -0
- package/dist/shell/search/index.js +1 -0
- package/dist/shell/telemetry.d.ts +7 -0
- package/dist/shell/telemetry.d.ts.map +1 -0
- package/dist/shell/telemetry.js +25 -0
- package/dist/shell/types.d.ts +110 -0
- package/dist/shell/types.d.ts.map +1 -0
- package/dist/shell/types.js +4 -0
- package/dist/tailwind/index.d.ts +20 -0
- package/dist/tailwind/index.d.ts.map +1 -0
- package/dist/tailwind/index.js +42 -0
- package/dist/types/keycloak.d.ts +26 -0
- package/dist/types/keycloak.d.ts.map +1 -0
- package/dist/types/keycloak.js +1 -0
- package/dist/types/platform.d.ts +83 -0
- package/dist/types/platform.d.ts.map +1 -0
- package/dist/types/platform.js +5 -0
- package/dist/vite/config.d.ts +71 -0
- package/dist/vite/config.d.ts.map +1 -0
- package/dist/vite/config.js +87 -0
- package/dist/vite/index.d.ts +18 -0
- package/dist/vite/index.d.ts.map +1 -0
- package/dist/vite/index.js +17 -0
- package/dist/vite/plugins.d.ts +44 -0
- package/dist/vite/plugins.d.ts.map +1 -0
- package/dist/vite/plugins.js +74 -0
- 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 @@
|
|
|
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 @@
|
|
|
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"}
|