@kyro-cms/admin 0.12.4 → 0.12.5
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/dist/index.cjs +64 -26
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +64 -26
- package/package.json +9 -3
- package/src/components/ActionBar.tsx +16 -16
- package/src/components/ApiHealth.tsx +522 -0
- package/src/components/ApiKeysManager.tsx +6 -4
- package/src/components/AuditLogsPage.tsx +4 -2
- package/src/components/AutoForm.tsx +10 -7
- package/src/components/BrandingHub.tsx +3 -1
- package/src/components/Dashboard.tsx +44 -37
- package/src/components/DashboardMetrics.tsx +168 -58
- package/src/components/DetailView.tsx +41 -38
- package/src/components/DeveloperCenter.tsx +5 -3
- package/src/components/GraphQLPlayground.tsx +10 -8
- package/src/components/ListView.tsx +66 -45
- package/src/components/LoginPage.tsx +21 -19
- package/src/components/MarketplaceManager.tsx +3 -1
- package/src/components/MediaGallery.tsx +12 -9
- package/src/components/PluginsManager.tsx +6 -4
- package/src/components/RestPlayground.tsx +8 -6
- package/src/components/SessionsManager.tsx +3 -1
- package/src/components/Sidebar.astro +180 -92
- package/src/components/UserManagement.tsx +8 -6
- package/src/components/UserMenu.tsx +19 -17
- package/src/components/VersionHistoryPanel.tsx +6 -4
- package/src/components/WebhookManager.tsx +11 -10
- package/src/components/autoform/AutoFormApiView.tsx +3 -1
- package/src/components/autoform/AutoFormEditView.tsx +3 -1
- package/src/components/autoform/AutoFormHeader.tsx +6 -6
- package/src/components/blocks/AccordionBlock.tsx +5 -3
- package/src/components/blocks/ArrayBlock.tsx +5 -3
- package/src/components/blocks/BlockWrapper.tsx +5 -3
- package/src/components/blocks/CodeBlock.tsx +4 -2
- package/src/components/blocks/FileBlock.tsx +5 -3
- package/src/components/blocks/HeroBlock.tsx +5 -3
- package/src/components/blocks/ImageBlock.tsx +6 -4
- package/src/components/blocks/ListBlock.tsx +5 -3
- package/src/components/blocks/ParagraphBlock.tsx +3 -1
- package/src/components/blocks/RelationshipBlock.tsx +5 -3
- package/src/components/blocks/RichTextBlock.tsx +5 -3
- package/src/components/blocks/VideoBlock.tsx +5 -3
- package/src/components/fields/AccordionField.tsx +10 -51
- package/src/components/fields/ArrayField.tsx +9 -7
- package/src/components/fields/ArrayLayout.tsx +3 -1
- package/src/components/fields/BlocksField.tsx +24 -75
- package/src/components/fields/ButtonField.tsx +3 -1
- package/src/components/fields/CardField.tsx +6 -4
- package/src/components/fields/GroupLayout.tsx +2 -49
- package/src/components/fields/HeadingField.tsx +3 -1
- package/src/components/fields/HeadingSubheadingField.tsx +4 -2
- package/src/components/fields/HeroField.tsx +8 -6
- package/src/components/fields/JSONField.tsx +9 -10
- package/src/components/fields/LinkField.tsx +4 -2
- package/src/components/fields/ListField.tsx +3 -1
- package/src/components/fields/MarkdownField.tsx +3 -17
- package/src/components/fields/RelationshipBlockField.tsx +7 -5
- package/src/components/fields/RelationshipField.tsx +3 -1
- package/src/components/fields/RichTextField.tsx +19 -16
- package/src/components/fields/SecretField.tsx +9 -69
- package/src/components/fields/SelectField.tsx +11 -6
- package/src/components/fields/TabsLayout.tsx +2 -48
- package/src/components/fields/TextField.tsx +9 -7
- package/src/components/fields/UploadField.tsx +3 -1
- package/src/components/fields/VideoField.tsx +6 -4
- package/src/components/ui/BlockDrawer.tsx +3 -1
- package/src/components/ui/CommandPalette.tsx +9 -7
- package/src/components/ui/CommandPaletteWrapper.tsx +3 -1
- package/src/components/ui/IconPickerModal.tsx +4 -2
- package/src/components/ui/PageHeader.tsx +48 -19
- package/src/components/users/UserDetail.tsx +3 -1
- package/src/components/users/UserForm.tsx +5 -3
- package/src/fields/examples/sample-field-2.tsx +3 -1
- package/src/fields/examples/sample-field.tsx +3 -1
- package/src/hooks/useAutoFormState.ts +19 -11
- package/src/integration.ts +21 -0
- package/src/layouts/AdminLayout.astro +252 -189
- package/src/layouts/AuthLayout.astro +52 -31
- package/src/lib/api.ts +7 -7
- package/src/lib/deep-equal.ts +3 -3
- package/src/lib/i18n.ts +44 -0
- package/src/lib/vite-shim-plugin.ts +3 -0
- package/src/locales/de.json +163 -0
- package/src/locales/en.json +163 -0
- package/src/locales/es.json +163 -0
- package/src/locales/fr.json +163 -0
- package/src/locales/pt.json +163 -0
- package/src/pages/403.astro +2 -1
- package/src/pages/[collection]/[id].astro +23 -10
- package/src/pages/[collection]/index.astro +48 -34
- package/src/pages/audit/index.astro +2 -1
- package/src/pages/graphql-explorer.astro +2 -1
- package/src/pages/graphql.astro +2 -1
- package/src/pages/health.astro +9 -0
- package/src/pages/index.astro +6 -5
- package/src/pages/keys.astro +2 -1
- package/src/pages/marketplace.astro +2 -1
- package/src/pages/media.astro +2 -1
- package/src/pages/plugins.astro +2 -1
- package/src/pages/rest-playground.astro +2 -1
- package/src/pages/roles/index.astro +16 -52
- package/src/pages/sessions.astro +2 -1
- package/src/pages/settings/[slug].astro +45 -30
- package/src/pages/users/[id].astro +1 -0
- package/src/pages/users/index.astro +2 -1
- package/src/pages/users/new.astro +2 -1
- package/src/pages/webhooks.astro +2 -1
- package/src/pages/index_ALT.astro +0 -3
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { Modal, ModalContent, ModalActions } from "./ui/Modal";
|
|
15
15
|
import { PageHeader } from "./ui/PageHeader";
|
|
16
16
|
import { Badge } from "./ui/Badge";
|
|
17
|
+
import { useTranslation } from "react-i18next";
|
|
17
18
|
|
|
18
19
|
interface Plugin {
|
|
19
20
|
id: string;
|
|
@@ -31,6 +32,7 @@ interface ToggleError {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export function PluginsManager() {
|
|
35
|
+
const { t } = useTranslation();
|
|
34
36
|
const [plugins, setPlugins] = useState<Plugin[]>([]);
|
|
35
37
|
const [loading, setLoading] = useState(true);
|
|
36
38
|
const [searchQuery, setSearchQuery] = useState("");
|
|
@@ -71,7 +73,7 @@ export function PluginsManager() {
|
|
|
71
73
|
return (
|
|
72
74
|
<div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-32">
|
|
73
75
|
<PageHeader
|
|
74
|
-
title="Plugins"
|
|
76
|
+
title={t("tooltips.plugins", { defaultValue: "Plugins" })}
|
|
75
77
|
description="Extend Kyro CMS with modular features and integrations."
|
|
76
78
|
icon={Blocks}
|
|
77
79
|
/>
|
|
@@ -85,7 +87,7 @@ export function PluginsManager() {
|
|
|
85
87
|
return (
|
|
86
88
|
<div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-32">
|
|
87
89
|
<PageHeader
|
|
88
|
-
title="Plugins"
|
|
90
|
+
title={t("tooltips.plugins", { defaultValue: "Plugins" })}
|
|
89
91
|
description="Extend Kyro CMS with modular features and integrations."
|
|
90
92
|
icon={Blocks}
|
|
91
93
|
actions={[
|
|
@@ -164,7 +166,7 @@ export function PluginsManager() {
|
|
|
164
166
|
<Search className="w-4 h-4 absolute left-3.5 top-1/2 -translate-y-1/2 text-[var(--kyro-text-secondary)] opacity-40" />
|
|
165
167
|
<input
|
|
166
168
|
type="text"
|
|
167
|
-
placeholder="Search plugins..."
|
|
169
|
+
placeholder={t("fields.searchPlugins", { defaultValue: "Search plugins..." })}
|
|
168
170
|
value={searchQuery}
|
|
169
171
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
170
172
|
className="w-full pl-10 pr-4 py-3 bg-[var(--kyro-bg-secondary)] border border-[var(--kyro-border)] rounded-xl text-xs focus:outline-none focus:border-[var(--kyro-primary)]/50 transition-all"
|
|
@@ -247,7 +249,7 @@ export function PluginsManager() {
|
|
|
247
249
|
<Modal
|
|
248
250
|
open={!!showConfigModal}
|
|
249
251
|
onClose={() => setShowConfigModal(null)}
|
|
250
|
-
title="Plugin Configuration"
|
|
252
|
+
title={t("tooltips.pluginConfiguration", { defaultValue: "Plugin Configuration" })}
|
|
251
253
|
size="lg"
|
|
252
254
|
>
|
|
253
255
|
<ModalContent>
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
X,
|
|
12
12
|
ChevronRight,
|
|
13
13
|
} from "./ui/icons";
|
|
14
|
+
import { useTranslation } from "react-i18next";
|
|
14
15
|
|
|
15
16
|
interface EnvVariable {
|
|
16
17
|
key: string;
|
|
@@ -78,6 +79,7 @@ const METHOD_BG: Record<string, string> = {
|
|
|
78
79
|
};
|
|
79
80
|
|
|
80
81
|
export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
82
|
+
const { t } = useTranslation();
|
|
81
83
|
const [sidebarTab, setSidebarTab] = useState<"collections" | "saved" | "history" | "env">("collections");
|
|
82
84
|
const [folders, setFolders] = useState<RequestFolder[]>([]);
|
|
83
85
|
const [history, setHistory] = useState<HistoryItem[]>([]);
|
|
@@ -357,7 +359,7 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
357
359
|
<button
|
|
358
360
|
onClick={() => setShowSidebar(!showSidebar)}
|
|
359
361
|
className="p-1 rounded-lg text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]"
|
|
360
|
-
title="Toggle sidebar"
|
|
362
|
+
title={t("tooltips.toggleSidebar", { defaultValue: "Toggle sidebar" })}
|
|
361
363
|
>
|
|
362
364
|
<ChevronRight className={`w-4 h-4 transition-transform ${showSidebar ? "rotate-180" : ""}`} />
|
|
363
365
|
</button>
|
|
@@ -694,10 +696,10 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
694
696
|
</>
|
|
695
697
|
)}
|
|
696
698
|
<div className="ml-auto flex items-center gap-1">
|
|
697
|
-
<button onClick={handleCopyResponse} className="p-1 rounded text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title="Copy response">
|
|
699
|
+
<button onClick={handleCopyResponse} className="p-1 rounded text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title={t("tooltips.copyResponse", { defaultValue: "Copy response" })}>
|
|
698
700
|
{copied ? <Check className="w-3 h-3 text-[var(--kyro-success)]" /> : <Copy className="w-3 h-3" />}
|
|
699
701
|
</button>
|
|
700
|
-
<button onClick={handleDownloadResponse} className="p-1 rounded text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title="Download response">
|
|
702
|
+
<button onClick={handleDownloadResponse} className="p-1 rounded text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)]" title={t("tooltips.downloadResponse", { defaultValue: "Download response" })}>
|
|
701
703
|
<Download className="w-3 h-3" />
|
|
702
704
|
</button>
|
|
703
705
|
</div>
|
|
@@ -727,7 +729,7 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
727
729
|
</div>
|
|
728
730
|
|
|
729
731
|
{/* Modals */}
|
|
730
|
-
<Modal open={showFolderModal} onClose={() => setShowFolderModal(false)} title="Create Folder" size="sm"
|
|
732
|
+
<Modal open={showFolderModal} onClose={() => setShowFolderModal(false)} title={t("tooltips.createFolder", { defaultValue: "Create Folder" })} size="sm"
|
|
731
733
|
footer={
|
|
732
734
|
<div className="flex gap-2">
|
|
733
735
|
<button onClick={() => setShowFolderModal(false)} className="kyro-btn kyro-btn-md kyro-btn-ghost">Cancel</button>
|
|
@@ -736,11 +738,11 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
736
738
|
}
|
|
737
739
|
>
|
|
738
740
|
<input type="text" value={newFolderName} onChange={(e) => setNewFolderName(e.target.value)}
|
|
739
|
-
placeholder="Folder name..." className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-3 py-2 text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
|
|
741
|
+
placeholder={t("fields.folderName", { defaultValue: "Folder name..." })} className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-3 py-2 text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
|
|
740
742
|
/>
|
|
741
743
|
</Modal>
|
|
742
744
|
|
|
743
|
-
<Modal open={showSaveModal} onClose={() => setShowSaveModal(false)} title="Save Request" size="sm"
|
|
745
|
+
<Modal open={showSaveModal} onClose={() => setShowSaveModal(false)} title={t("tooltips.saveRequest", { defaultValue: "Save Request" })} size="sm"
|
|
744
746
|
footer={
|
|
745
747
|
<div className="flex gap-2">
|
|
746
748
|
<button onClick={() => setShowSaveModal(false)} className="kyro-btn kyro-btn-md kyro-btn-ghost">Cancel</button>
|
|
@@ -4,6 +4,7 @@ import { Shield, Monitor, Trash2, Clock, AlertTriangle, Info, LogOut, Globe, Act
|
|
|
4
4
|
import { PageHeader } from "./ui/PageHeader";
|
|
5
5
|
import { toast } from "../lib/stores";
|
|
6
6
|
import { Badge } from "./ui/Badge";
|
|
7
|
+
import { useTranslation } from "react-i18next";
|
|
7
8
|
|
|
8
9
|
interface Session {
|
|
9
10
|
id: string;
|
|
@@ -33,6 +34,7 @@ function timeAgo(ts: number): string {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export function SessionsManager() {
|
|
37
|
+
const { t } = useTranslation();
|
|
36
38
|
const [sessions, setSessions] = useState<Session[]>([]);
|
|
37
39
|
const [loading, setLoading] = useState(true);
|
|
38
40
|
const [error, setError] = useState("");
|
|
@@ -82,7 +84,7 @@ export function SessionsManager() {
|
|
|
82
84
|
return (
|
|
83
85
|
<div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-32">
|
|
84
86
|
<PageHeader
|
|
85
|
-
title="Active Sessions"
|
|
87
|
+
title={t("tooltips.activeSessions", { defaultValue: "Active Sessions" })}
|
|
86
88
|
description="Monitor and manage your cryptographic access across all devices."
|
|
87
89
|
icon={Monitor}
|
|
88
90
|
actions={otherCount > 0 ? [
|
|
@@ -21,6 +21,9 @@ const darkLogo = brandSettings?.identity?.darkLogo;
|
|
|
21
21
|
const logoWidth = siteLogo?.width || darkLogo?.width;
|
|
22
22
|
const logoHeight = siteLogo?.height || darkLogo?.height;
|
|
23
23
|
const logoAlt = siteLogo?.altText || darkLogo?.altText || siteName;
|
|
24
|
+
const showSiteName = brandSettings?.identity?.showSiteName ?? true;
|
|
25
|
+
|
|
26
|
+
import i18next from "../lib/i18n";
|
|
24
27
|
|
|
25
28
|
interface Props {
|
|
26
29
|
title: string;
|
|
@@ -29,38 +32,59 @@ interface Props {
|
|
|
29
32
|
const { title } = Astro.props;
|
|
30
33
|
const currentPath = Astro.url.pathname;
|
|
31
34
|
|
|
32
|
-
const
|
|
35
|
+
const groupedCollections = new Map<string, NavItem[]>();
|
|
36
|
+
const defaultCollections: NavItem[] = [];
|
|
37
|
+
|
|
38
|
+
nonAuthCollections
|
|
33
39
|
.filter((col) => col.slug !== "media") //removed here cos it's been added manually in the navSections.
|
|
34
|
-
.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
.forEach((col) => {
|
|
41
|
+
const item: NavItem = {
|
|
42
|
+
href: `${adminPath}/${col.slug}`,
|
|
43
|
+
label: col.label || col.slug,
|
|
44
|
+
icon: (col.admin?.icon as string) || "collection",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (col.admin?.group) {
|
|
48
|
+
const groupName = col.admin.group as string;
|
|
49
|
+
if (!groupedCollections.has(groupName)) {
|
|
50
|
+
groupedCollections.set(groupName, []);
|
|
51
|
+
}
|
|
52
|
+
groupedCollections.get(groupName)!.push(item);
|
|
53
|
+
} else {
|
|
54
|
+
defaultCollections.push(item);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const dynamicSections = Array.from(groupedCollections.entries()).map(([group, items]) => ({
|
|
59
|
+
label: group,
|
|
60
|
+
items: items,
|
|
61
|
+
}));
|
|
39
62
|
|
|
40
63
|
const navSections: { label: string; items: NavItem[] }[] = [
|
|
41
64
|
{
|
|
42
|
-
label: "
|
|
65
|
+
label: "Dashboard",
|
|
43
66
|
items: [
|
|
44
|
-
{ href: adminPath, label: "
|
|
67
|
+
{ href: adminPath, label: "nav.dashboard", icon: "home" },
|
|
45
68
|
{
|
|
46
69
|
href: adminPath + "/media",
|
|
47
|
-
label: "
|
|
70
|
+
label: "nav.media",
|
|
48
71
|
icon: "media",
|
|
49
72
|
},
|
|
50
73
|
],
|
|
51
74
|
},
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
...dynamicSections,
|
|
76
|
+
...(defaultCollections.length > 0 ? [{
|
|
77
|
+
label: "Content",
|
|
78
|
+
items: defaultCollections,
|
|
79
|
+
}] : []),
|
|
56
80
|
{
|
|
57
81
|
label: "Settings",
|
|
58
82
|
items: [
|
|
59
|
-
{ href: adminPath + "/users", label: "
|
|
60
|
-
{ href: adminPath + "/plugins", label: "
|
|
83
|
+
{ href: adminPath + "/users", label: "nav.users", icon: "users" },
|
|
84
|
+
{ href: adminPath + "/plugins", label: "nav.plugins", icon: "plugins" },
|
|
61
85
|
{
|
|
62
86
|
href: adminPath + "/settings",
|
|
63
|
-
label: "
|
|
87
|
+
label: "nav.settings",
|
|
64
88
|
icon: "settings",
|
|
65
89
|
},
|
|
66
90
|
],
|
|
@@ -101,36 +125,47 @@ function isActive(item: NavItem): boolean {
|
|
|
101
125
|
<div class="px-6 md:px-8 py-6 md:py-8 flex items-center justify-between gap-3">
|
|
102
126
|
{
|
|
103
127
|
siteLogo || darkLogo ? (
|
|
104
|
-
<div class="flex items-center">
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
<div class="flex items-center gap-3">
|
|
129
|
+
<div class="flex items-center">
|
|
130
|
+
{siteLogo && (
|
|
131
|
+
<img
|
|
132
|
+
src={siteLogo.url}
|
|
133
|
+
alt={logoAlt}
|
|
134
|
+
style={{
|
|
135
|
+
width: logoWidth ? `${logoWidth}px` : "auto",
|
|
136
|
+
height: logoHeight ? `${logoHeight}px` : "32px",
|
|
137
|
+
objectFit: "contain",
|
|
138
|
+
}}
|
|
139
|
+
class={`rounded-lg ${darkLogo ? "block dark:hidden" : ""}`}
|
|
140
|
+
/>
|
|
141
|
+
)}
|
|
142
|
+
{darkLogo && (
|
|
143
|
+
<img
|
|
144
|
+
src={darkLogo.url}
|
|
145
|
+
alt={logoAlt}
|
|
146
|
+
style={{
|
|
147
|
+
width: logoWidth ? `${logoWidth}px` : "auto",
|
|
148
|
+
height: logoHeight ? `${logoHeight}px` : "32px",
|
|
149
|
+
objectFit: "contain",
|
|
150
|
+
}}
|
|
151
|
+
class={`rounded-lg ${siteLogo ? "hidden dark:block" : ""}`}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
{showSiteName && (
|
|
156
|
+
<span class="text-xl font-bold tracking-tight text-[var(--kyro-text-primary)]">
|
|
157
|
+
{siteName}
|
|
158
|
+
</span>
|
|
128
159
|
)}
|
|
129
160
|
</div>
|
|
130
161
|
) : (
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
162
|
+
<div class="flex items-center gap-3">
|
|
163
|
+
<img src="/logo-white.svg" alt="Kyro CMS" class="w-7 h-7 hidden dark:block" />
|
|
164
|
+
<img src="/logo.svg" alt="Kyro CMS" class="w-7 h-7 block dark:hidden" />
|
|
165
|
+
<span class="text-2xl font-black tracking-tighter text-[var(--kyro-text-primary)]">
|
|
166
|
+
{siteName}
|
|
167
|
+
</span>
|
|
168
|
+
</div>
|
|
134
169
|
)
|
|
135
170
|
}
|
|
136
171
|
<button id="mobile-close-btn" class="md:hidden p-2 -mr-2 rounded-lg text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] transition-colors">
|
|
@@ -142,43 +177,54 @@ function isActive(item: NavItem): boolean {
|
|
|
142
177
|
<div class="space-y-4">
|
|
143
178
|
{
|
|
144
179
|
navSections.map((section) => (
|
|
145
|
-
<div class="space-y-2" data-section={section.label}>
|
|
180
|
+
<div class="space-y-2 group/section" data-section={section.label}>
|
|
146
181
|
<div class="pt-4 pb-2">
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
182
|
+
<button type="button" class="w-full flex items-center justify-between px-6 focus:outline-none sidebar-section-toggle transition-opacity hover:opacity-80">
|
|
183
|
+
<p class="text-[10px] font-medium text-[var(--kyro-text-secondary)] tracking-[0.2em] opacity-40 uppercase">
|
|
184
|
+
{i18next.t(section.label, { defaultValue: section.label.split('.').pop()?.toUpperCase() || section.label }) || (section.label.split('.').pop()?.toUpperCase() || section.label)}
|
|
185
|
+
</p>
|
|
186
|
+
<Icons.ChevronDown className="w-3.5 h-3.5 text-[var(--kyro-text-secondary)] opacity-40 transition-transform duration-200 group-data-[collapsed=true]/section:-rotate-90" />
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
<div class="section-items space-y-1">
|
|
190
|
+
{section.items.map((item) => {
|
|
191
|
+
const slug = item.href.split("/").pop();
|
|
192
|
+
const type = item.href.includes("/settings")
|
|
193
|
+
? "global"
|
|
194
|
+
: "collection";
|
|
195
|
+
return (
|
|
196
|
+
<a
|
|
197
|
+
href={item.href}
|
|
198
|
+
data-nav-item
|
|
199
|
+
data-slug={slug}
|
|
200
|
+
data-type={type}
|
|
201
|
+
class={`flex items-center gap-4 px-6 py-2 rounded-2xl transition-all font-medium text-[13px] ${
|
|
202
|
+
item.icon === "collection"
|
|
203
|
+
? currentPath === item.href ||
|
|
204
|
+
currentPath.startsWith(item.href + "/")
|
|
205
|
+
? "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-primary)] active-nav-item"
|
|
206
|
+
: "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"
|
|
207
|
+
: isActive(item)
|
|
208
|
+
? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-lg active-nav-item"
|
|
209
|
+
: "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"
|
|
210
|
+
}`}
|
|
211
|
+
>
|
|
212
|
+
{(() => {
|
|
213
|
+
const iconName = iconAliases[item.icon] || item.icon;
|
|
214
|
+
const Icon = (Icons as any)[iconName] || Icons.Dot;
|
|
215
|
+
return <Icon className="w-4 h-4" strokeWidth={2.5} />;
|
|
216
|
+
})()}
|
|
217
|
+
<span>
|
|
218
|
+
{(() => {
|
|
219
|
+
const fallback = item.label.split('.').pop() || item.label;
|
|
220
|
+
const capitalized = fallback.charAt(0).toUpperCase() + fallback.slice(1);
|
|
221
|
+
return i18next.t(item.label, { defaultValue: capitalized }) || capitalized;
|
|
222
|
+
})()}
|
|
223
|
+
</span>
|
|
224
|
+
</a>
|
|
225
|
+
);
|
|
226
|
+
})}
|
|
150
227
|
</div>
|
|
151
|
-
{section.items.map((item) => {
|
|
152
|
-
const slug = item.href.split("/").pop();
|
|
153
|
-
const type = item.href.includes("/settings")
|
|
154
|
-
? "global"
|
|
155
|
-
: "collection";
|
|
156
|
-
return (
|
|
157
|
-
<a
|
|
158
|
-
href={item.href}
|
|
159
|
-
data-nav-item
|
|
160
|
-
data-slug={slug}
|
|
161
|
-
data-type={type}
|
|
162
|
-
class={`flex items-center gap-4 px-6 py-2 rounded-2xl transition-all font-medium text-[13px] ${
|
|
163
|
-
item.icon === "collection"
|
|
164
|
-
? currentPath === item.href ||
|
|
165
|
-
currentPath.startsWith(item.href + "/")
|
|
166
|
-
? "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-primary)]"
|
|
167
|
-
: "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"
|
|
168
|
-
: isActive(item)
|
|
169
|
-
? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] shadow-lg"
|
|
170
|
-
: "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] hover:text-[var(--kyro-text-primary)]"
|
|
171
|
-
}`}
|
|
172
|
-
>
|
|
173
|
-
{(() => {
|
|
174
|
-
const iconName = iconAliases[item.icon] || item.icon;
|
|
175
|
-
const Icon = (Icons as any)[iconName] || Icons.Dot;
|
|
176
|
-
return <Icon className="w-4 h-4" strokeWidth={2.5} />;
|
|
177
|
-
})()}
|
|
178
|
-
<span>{item.label}</span>
|
|
179
|
-
</a>
|
|
180
|
-
);
|
|
181
|
-
})}
|
|
182
228
|
</div>
|
|
183
229
|
))
|
|
184
230
|
}
|
|
@@ -186,42 +232,84 @@ function isActive(item: NavItem): boolean {
|
|
|
186
232
|
</nav>
|
|
187
233
|
|
|
188
234
|
<script>
|
|
235
|
+
function initSidebar() {
|
|
236
|
+
const sections = document.querySelectorAll("[data-section]");
|
|
237
|
+
sections.forEach((section) => {
|
|
238
|
+
const label = section.getAttribute("data-section");
|
|
239
|
+
const toggleBtn = section.querySelector('.sidebar-section-toggle');
|
|
240
|
+
const itemsContainer = section.querySelector('.section-items');
|
|
241
|
+
|
|
242
|
+
const isCollapsed = localStorage.getItem(`kyro-sidebar-collapsed-${label}`) === 'true';
|
|
243
|
+
if (isCollapsed) {
|
|
244
|
+
section.setAttribute('data-collapsed', 'true');
|
|
245
|
+
if (itemsContainer) itemsContainer.classList.add('hidden');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const activeItem = section.querySelector('.active-nav-item');
|
|
249
|
+
if (activeItem && isCollapsed) {
|
|
250
|
+
section.removeAttribute('data-collapsed');
|
|
251
|
+
if (itemsContainer) itemsContainer.classList.remove('hidden');
|
|
252
|
+
localStorage.setItem(`kyro-sidebar-collapsed-${label}`, 'false');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Remove old listeners to prevent duplicates
|
|
256
|
+
const newToggleBtn = toggleBtn?.cloneNode(true);
|
|
257
|
+
if (toggleBtn && newToggleBtn && toggleBtn.parentNode) {
|
|
258
|
+
toggleBtn.parentNode.replaceChild(newToggleBtn, toggleBtn);
|
|
259
|
+
newToggleBtn.addEventListener('click', () => {
|
|
260
|
+
const currentlyCollapsed = section.getAttribute('data-collapsed') === 'true';
|
|
261
|
+
if (currentlyCollapsed) {
|
|
262
|
+
section.removeAttribute('data-collapsed');
|
|
263
|
+
if (itemsContainer) itemsContainer.classList.remove('hidden');
|
|
264
|
+
localStorage.setItem(`kyro-sidebar-collapsed-${label}`, 'false');
|
|
265
|
+
} else {
|
|
266
|
+
section.setAttribute('data-collapsed', 'true');
|
|
267
|
+
if (itemsContainer) itemsContainer.classList.add('hidden');
|
|
268
|
+
localStorage.setItem(`kyro-sidebar-collapsed-${label}`, 'true');
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Run on load and view transitions
|
|
276
|
+
initSidebar();
|
|
277
|
+
document.addEventListener('astro:page-load', initSidebar);
|
|
278
|
+
|
|
189
279
|
// Handle frontend RBAC pruning in the sidebar
|
|
190
|
-
window.addEventListener("kyro:auth-ready", (event
|
|
280
|
+
window.addEventListener("kyro:auth-ready", (event) => {
|
|
281
|
+
// @ts-ignore
|
|
191
282
|
const { permissions } = event.detail;
|
|
192
283
|
if (!permissions) return;
|
|
193
284
|
|
|
194
285
|
const navItems = document.querySelectorAll("[data-nav-item]");
|
|
195
|
-
navItems.forEach((item
|
|
286
|
+
navItems.forEach((item) => {
|
|
196
287
|
const slug = item.getAttribute("data-slug");
|
|
197
288
|
const type = item.getAttribute("data-type");
|
|
198
289
|
|
|
199
290
|
let hasAccess = true;
|
|
200
|
-
|
|
201
291
|
if (type === "collection" && permissions.collections) {
|
|
202
292
|
const colPerms = permissions.collections[slug];
|
|
203
|
-
if (colPerms && colPerms.read === false)
|
|
204
|
-
hasAccess = false;
|
|
205
|
-
}
|
|
293
|
+
if (colPerms && colPerms.read === false) hasAccess = false;
|
|
206
294
|
} else if (type === "global" && permissions.globals) {
|
|
207
295
|
const globalPerms = permissions.globals[slug];
|
|
208
|
-
if (globalPerms && globalPerms.read === false)
|
|
209
|
-
hasAccess = false;
|
|
210
|
-
}
|
|
296
|
+
if (globalPerms && globalPerms.read === false) hasAccess = false;
|
|
211
297
|
}
|
|
212
298
|
|
|
213
299
|
if (!hasAccess) {
|
|
300
|
+
// @ts-ignore
|
|
214
301
|
item.style.display = "none";
|
|
215
302
|
}
|
|
216
303
|
});
|
|
217
304
|
|
|
218
|
-
// Hide empty sections
|
|
305
|
+
// Hide empty sections after RBAC
|
|
219
306
|
const sections = document.querySelectorAll("[data-section]");
|
|
220
|
-
sections.forEach((section
|
|
307
|
+
sections.forEach((section) => {
|
|
221
308
|
const visibleItems = section.querySelectorAll(
|
|
222
|
-
'[data-nav-item]:not([style*="display: none"])'
|
|
309
|
+
'[data-nav-item]:not([style*="display: none"])'
|
|
223
310
|
);
|
|
224
311
|
if (visibleItems.length === 0) {
|
|
312
|
+
// @ts-ignore
|
|
225
313
|
section.style.display = "none";
|
|
226
314
|
}
|
|
227
315
|
});
|
|
@@ -238,14 +326,14 @@ function isActive(item: NavItem): boolean {
|
|
|
238
326
|
<button
|
|
239
327
|
id="theme-light-btn"
|
|
240
328
|
class="p-2 rounded-lg transition-all active:scale-95"
|
|
241
|
-
title="Light Mode"
|
|
329
|
+
title={i18next.t("sidebar.lightMode", { defaultValue: "Light Mode" })}
|
|
242
330
|
>
|
|
243
331
|
<Icons.Sun className="w-4 h-4" strokeWidth={2.5} />
|
|
244
332
|
</button>
|
|
245
333
|
<button
|
|
246
334
|
id="theme-dark-btn"
|
|
247
335
|
class="p-2 rounded-lg transition-all active:scale-95"
|
|
248
|
-
title="Dark Mode"
|
|
336
|
+
title={i18next.t("sidebar.darkMode", { defaultValue: "Dark Mode" })}
|
|
249
337
|
>
|
|
250
338
|
<Icons.Moon className="w-4 h-4" strokeWidth={2.5} />
|
|
251
339
|
</button>
|
|
@@ -15,6 +15,7 @@ import { useUIStore, toast } from "../lib/stores";
|
|
|
15
15
|
import { Modal, ModalContent, ModalActions } from "./ui/Modal";
|
|
16
16
|
import { Badge } from "./ui/Badge";
|
|
17
17
|
import { PageHeader } from "./ui/PageHeader";
|
|
18
|
+
import { useTranslation } from "react-i18next";
|
|
18
19
|
|
|
19
20
|
interface User {
|
|
20
21
|
id: string;
|
|
@@ -29,6 +30,7 @@ interface User {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export function UserManagement() {
|
|
33
|
+
const { t } = useTranslation();
|
|
32
34
|
const [users, setUsers] = useState<User[]>([]);
|
|
33
35
|
const [loading, setLoading] = useState(true);
|
|
34
36
|
const [searchQuery, setSearchQuery] = useState("");
|
|
@@ -135,7 +137,7 @@ export function UserManagement() {
|
|
|
135
137
|
<div className="w-full space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-700 px-4 md:px-8 pb-12">
|
|
136
138
|
{/* Header */}
|
|
137
139
|
<PageHeader
|
|
138
|
-
title="Identity & Access"
|
|
140
|
+
title={t("tooltips.identityAccess", { defaultValue: "Identity & Access" })}
|
|
139
141
|
description="Manage the core administrative team and security permissions."
|
|
140
142
|
icon={Users}
|
|
141
143
|
action={{
|
|
@@ -155,7 +157,7 @@ export function UserManagement() {
|
|
|
155
157
|
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-secondary)] opacity-40 group-focus-within:opacity-100 transition-opacity" />
|
|
156
158
|
<input
|
|
157
159
|
type="text"
|
|
158
|
-
placeholder="Search by identity or email..."
|
|
160
|
+
placeholder={t("fields.searchByIdentityOr", { defaultValue: "Search by identity or email..." })}
|
|
159
161
|
value={searchQuery}
|
|
160
162
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
161
163
|
className="w-full pl-10 pr-4 py-2.5 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-xl focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] transition-all text-xs font-bold"
|
|
@@ -241,7 +243,7 @@ export function UserManagement() {
|
|
|
241
243
|
<button
|
|
242
244
|
onClick={() => handleDelete(user)}
|
|
243
245
|
className="p-1.5 rounded-lg border border-red-500/20 bg-red-500/10 text-red-500 hover:bg-red-500/20 transition-all"
|
|
244
|
-
title="Delete User"
|
|
246
|
+
title={t("tooltips.deleteUser", { defaultValue: "Delete User" })}
|
|
245
247
|
>
|
|
246
248
|
<Trash2 className="w-3.5 h-3.5" />
|
|
247
249
|
</button>
|
|
@@ -258,7 +260,7 @@ export function UserManagement() {
|
|
|
258
260
|
<Modal
|
|
259
261
|
open={showCreateModal}
|
|
260
262
|
onClose={() => setShowCreateModal(false)}
|
|
261
|
-
title="Create User"
|
|
263
|
+
title={t("tooltips.createUser", { defaultValue: "Create User" })}
|
|
262
264
|
size="lg"
|
|
263
265
|
>
|
|
264
266
|
<ModalContent>
|
|
@@ -269,7 +271,7 @@ export function UserManagement() {
|
|
|
269
271
|
type="text"
|
|
270
272
|
value={createForm.name}
|
|
271
273
|
onChange={(e) => setCreateForm({ ...createForm, name: e.target.value })}
|
|
272
|
-
placeholder="John Doe"
|
|
274
|
+
placeholder={t("fields.johnDoe", { defaultValue: "John Doe" })}
|
|
273
275
|
className="w-full px-4 py-3 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-xl text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
|
|
274
276
|
/>
|
|
275
277
|
</div>
|
|
@@ -290,7 +292,7 @@ export function UserManagement() {
|
|
|
290
292
|
type="password"
|
|
291
293
|
value={createForm.password}
|
|
292
294
|
onChange={(e) => setCreateForm({ ...createForm, password: e.target.value })}
|
|
293
|
-
placeholder="Minimum 12 characters"
|
|
295
|
+
placeholder={t("fields.minimum12Characters", { defaultValue: "Minimum 12 characters" })}
|
|
294
296
|
required
|
|
295
297
|
minLength={12}
|
|
296
298
|
className="w-full px-4 py-3 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-xl text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
|