@kyro-cms/admin 0.3.2 → 0.3.4

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 (242) hide show
  1. package/dist/EditorClient-XEUOVAAC.js +466 -0
  2. package/dist/EditorClient-XEUOVAAC.js.map +1 -0
  3. package/dist/EditorClient-YLCGVDXY.cjs +468 -0
  4. package/dist/EditorClient-YLCGVDXY.cjs.map +1 -0
  5. package/dist/chunk-7KPIUCGT.js +384 -0
  6. package/dist/chunk-7KPIUCGT.js.map +1 -0
  7. package/dist/chunk-GOACG6R7.cjs +473 -0
  8. package/dist/chunk-GOACG6R7.cjs.map +1 -0
  9. package/dist/index.cjs +14861 -0
  10. package/dist/index.cjs.map +1 -0
  11. package/dist/index.css +1661 -0
  12. package/dist/index.css.map +1 -0
  13. package/dist/index.d.ts +563 -0
  14. package/dist/index.js +14784 -0
  15. package/dist/index.js.map +1 -0
  16. package/package.json +19 -19
  17. package/src/components/ActionBar.tsx +7 -43
  18. package/src/components/Admin.tsx +138 -277
  19. package/src/components/ApiKeysManager.tsx +428 -419
  20. package/src/components/AuditLogsPage.tsx +35 -39
  21. package/src/components/AuthBridge.tsx +51 -0
  22. package/src/components/AutoForm.tsx +495 -1230
  23. package/src/components/BrandingHub.tsx +18 -19
  24. package/src/components/BulkActionsBar.tsx +1 -1
  25. package/src/components/CreateView.tsx +22 -36
  26. package/src/components/Dashboard.tsx +60 -84
  27. package/src/components/DetailView.tsx +113 -91
  28. package/src/components/DeveloperCenter.tsx +200 -198
  29. package/src/components/FieldRenderer.tsx +206 -0
  30. package/src/components/GraphQLPlayground.tsx +340 -480
  31. package/src/components/ListView.tsx +828 -254
  32. package/src/components/LoginPage.tsx +3 -4
  33. package/src/components/MarketplaceManager.tsx +254 -0
  34. package/src/components/MediaGallery.tsx +856 -1192
  35. package/src/components/PluginsManager.tsx +277 -0
  36. package/src/components/RestPlayground.tsx +398 -560
  37. package/src/components/SessionsManager.tsx +211 -0
  38. package/src/components/Sidebar.astro +179 -151
  39. package/src/components/ThemeProvider.tsx +7 -161
  40. package/src/components/UserManagement.tsx +162 -146
  41. package/src/components/UserMenu.tsx +110 -0
  42. package/src/components/WebhookManager.tsx +305 -367
  43. package/src/components/blocks/AccordionBlock.tsx +4 -4
  44. package/src/components/blocks/ArrayBlock.tsx +3 -3
  45. package/src/components/blocks/BlockEditModal.tsx +8 -8
  46. package/src/components/blocks/BlockWrapper.tsx +61 -0
  47. package/src/components/blocks/ButtonBlock.tsx +4 -4
  48. package/src/components/blocks/ChildBlocksTree.tsx +23 -25
  49. package/src/components/blocks/CodeBlock.tsx +15 -15
  50. package/src/components/blocks/ColumnsBlock.tsx +6 -44
  51. package/src/components/blocks/DividerBlock.tsx +3 -3
  52. package/src/components/blocks/FileBlock.tsx +4 -4
  53. package/src/components/blocks/HeadingBlock.tsx +6 -38
  54. package/src/components/blocks/HeroBlock.tsx +4 -4
  55. package/src/components/blocks/ImageBlock.tsx +4 -4
  56. package/src/components/blocks/LinkBlock.tsx +4 -4
  57. package/src/components/blocks/ListBlock.tsx +3 -3
  58. package/src/components/blocks/ParagraphBlock.tsx +12 -42
  59. package/src/components/blocks/RelationshipBlock.tsx +4 -4
  60. package/src/components/blocks/RichTextBlock.tsx +4 -4
  61. package/src/components/blocks/VStackBlock.tsx +5 -37
  62. package/src/components/blocks/VideoBlock.tsx +4 -4
  63. package/src/components/blocks/types.ts +11 -0
  64. package/src/components/fields/AccordionField.tsx +1 -1
  65. package/src/components/fields/ArrayField.tsx +2 -2
  66. package/src/components/fields/ArrayLayout.tsx +93 -0
  67. package/src/components/fields/BlocksField.tsx +122 -111
  68. package/src/components/fields/ButtonField.tsx +1 -1
  69. package/src/components/fields/CheckboxField.tsx +14 -15
  70. package/src/components/fields/ChildrenField.tsx +2 -2
  71. package/src/components/fields/CodeField.tsx +3 -3
  72. package/src/components/fields/ColumnsField.tsx +2 -2
  73. package/src/components/fields/DateField.tsx +13 -26
  74. package/src/components/fields/EditorClient.tsx +26 -28
  75. package/src/components/fields/FieldLayout.tsx +52 -0
  76. package/src/components/fields/GroupLayout.tsx +35 -0
  77. package/src/components/fields/JSONField.tsx +7 -7
  78. package/src/components/fields/LinkField.tsx +1 -1
  79. package/src/components/fields/MarkdownField.tsx +1 -1
  80. package/src/components/fields/NumberField.tsx +13 -26
  81. package/src/components/fields/PortableTextField.tsx +4 -4
  82. package/src/components/fields/PortableTextRenderer.tsx +1 -1
  83. package/src/components/fields/RelationshipBlockField.tsx +31 -23
  84. package/src/components/fields/RelationshipField.tsx +14 -14
  85. package/src/components/fields/SelectField.tsx +17 -26
  86. package/src/components/fields/TabsLayout.tsx +69 -0
  87. package/src/components/fields/TextField.tsx +85 -38
  88. package/src/components/fields/UploadField.tsx +71 -41
  89. package/src/components/fields/VideoField.tsx +1 -1
  90. package/src/components/fields/extensions/blockComponents.tsx +2 -2
  91. package/src/components/fields/extensions/blocksStore.ts +207 -193
  92. package/src/components/fields/types.ts +22 -0
  93. package/src/components/layout/Layout.tsx +1 -1
  94. package/src/components/ui/ActionMenu.tsx +63 -0
  95. package/src/components/ui/Badge.tsx +59 -5
  96. package/src/components/ui/BlockDrawer.tsx +4 -5
  97. package/src/components/ui/CommandPalette.tsx +58 -36
  98. package/src/components/ui/CommandPaletteWrapper.tsx +18 -17
  99. package/src/components/ui/Dropdown.tsx +18 -16
  100. package/src/components/ui/EmptyState.tsx +25 -0
  101. package/src/components/ui/GlobalModal.tsx +49 -0
  102. package/src/components/ui/IconButton.tsx +44 -0
  103. package/src/components/ui/Modal.tsx +19 -20
  104. package/src/components/ui/PageHeader.tsx +158 -0
  105. package/src/components/ui/Pagination.tsx +61 -0
  106. package/src/components/ui/PromptModal.tsx +1 -1
  107. package/src/components/ui/SearchInput.tsx +57 -0
  108. package/src/components/ui/SeoPreview.tsx +31 -0
  109. package/src/components/ui/SessionModal.tsx +0 -0
  110. package/src/components/ui/SlidePanel.tsx +2 -0
  111. package/src/components/ui/Toast.tsx +65 -122
  112. package/src/components/ui/Toaster.tsx +18 -0
  113. package/src/components/ui/icons.tsx +112 -0
  114. package/src/components/users/UserDetail.tsx +290 -0
  115. package/src/components/users/UserForm.tsx +242 -0
  116. package/src/components/users/UsersList.tsx +338 -0
  117. package/src/env.d.ts +13 -13
  118. package/src/fields/index.ts +2 -1
  119. package/src/global.d.ts +7 -0
  120. package/src/hooks/data.ts +2 -9
  121. package/src/hooks/useAsyncData.ts +36 -0
  122. package/src/hooks/useAutoFormState.ts +527 -0
  123. package/src/hooks/useSelection.ts +49 -0
  124. package/src/hooks/useSession.ts +0 -0
  125. package/src/index.ts +11 -1
  126. package/src/integration.ts +86 -11
  127. package/src/kyro-cms.d.ts +209 -0
  128. package/src/layouts/AdminLayout.astro +128 -11
  129. package/src/layouts/AuthLayout.astro +21 -5
  130. package/src/lib/api.ts +175 -55
  131. package/src/lib/autoform-store.ts +435 -0
  132. package/src/lib/config.ts +82 -34
  133. package/src/lib/createRegistry.ts +29 -0
  134. package/src/lib/default-kyro-config.ts +4 -0
  135. package/src/lib/globals.ts +50 -0
  136. package/src/lib/media-utils.ts +18 -0
  137. package/src/lib/object-utils.ts +77 -0
  138. package/src/lib/paths.ts +61 -0
  139. package/src/lib/stores/index.ts +370 -0
  140. package/src/lib/types.ts +43 -0
  141. package/src/lib/useResourceManager.ts +105 -0
  142. package/src/pages/403.astro +67 -0
  143. package/src/pages/[collection]/[id].astro +14 -180
  144. package/src/pages/[collection]/index.astro +11 -6
  145. package/src/pages/api-explorer.astro +173 -0
  146. package/src/pages/audit/index.astro +2 -0
  147. package/src/pages/auth/login.astro +122 -0
  148. package/src/pages/auth/register.astro +167 -0
  149. package/src/pages/graphql-explorer.astro +59 -0
  150. package/src/pages/{admin/graphql.astro → graphql.astro} +51 -17
  151. package/src/pages/index.astro +577 -0
  152. package/src/pages/index_ALT.astro +3 -0
  153. package/src/pages/keys.astro +11 -0
  154. package/src/pages/marketplace.astro +11 -0
  155. package/src/pages/media.astro +3 -0
  156. package/src/pages/plugins.astro +8 -0
  157. package/src/pages/preview/[collection]/[id].astro +188 -123
  158. package/src/pages/rest-playground.astro +62 -0
  159. package/src/pages/roles/index.astro +183 -76
  160. package/src/pages/sessions.astro +8 -0
  161. package/src/pages/settings/[slug].astro +92 -114
  162. package/src/pages/settings/index.astro +5 -3
  163. package/src/pages/users/[id].astro +25 -154
  164. package/src/pages/users/index.astro +19 -130
  165. package/src/pages/users/new.astro +9 -86
  166. package/src/pages/webhooks.astro +11 -0
  167. package/src/routes.ts +80 -0
  168. package/src/styles/main.css +119 -79
  169. package/src/theme/tokens.ts +1 -0
  170. package/src/vite-env.d.ts +14 -0
  171. package/src/collections/auth/index.ts +0 -155
  172. package/src/collections/portfolio/index.ts +0 -343
  173. package/src/components/ApiExplorer.tsx +0 -325
  174. package/src/components/EnhancedListView.tsx +0 -889
  175. package/src/components/GraphQLExplorer.tsx +0 -675
  176. package/src/components/Icons.tsx +0 -23
  177. package/src/components/StatusBadge.tsx +0 -76
  178. package/src/lib/MediaService.ts +0 -541
  179. package/src/lib/auth/sqlite-adapter.ts +0 -319
  180. package/src/lib/dataStore.ts +0 -226
  181. package/src/lib/db/adapter.ts +0 -54
  182. package/src/lib/db/drizzle-mysql-adapter.ts +0 -194
  183. package/src/lib/db/drizzle-mysql-auth-adapter.ts +0 -327
  184. package/src/lib/db/drizzle-postgres-adapter.ts +0 -202
  185. package/src/lib/db/drizzle-postgres-auth-adapter.ts +0 -304
  186. package/src/lib/db/drizzle-sqlite-adapter.ts +0 -227
  187. package/src/lib/db/drizzle-sqlite-auth-adapter.ts +0 -548
  188. package/src/lib/db/index.ts +0 -449
  189. package/src/lib/db/mongodb-adapter.ts +0 -207
  190. package/src/lib/db/mongodb-auth-adapter.ts +0 -305
  191. package/src/lib/db/schema/mysql-auth.ts +0 -113
  192. package/src/lib/db/schema/mysql-content.ts +0 -20
  193. package/src/lib/db/schema/postgres-auth.ts +0 -116
  194. package/src/lib/db/schema/postgres-content.ts +0 -35
  195. package/src/lib/db/schema/postgres-media.ts +0 -52
  196. package/src/lib/db/schema/postgres-settings.ts +0 -11
  197. package/src/lib/db/schema/sqlite-auth.ts +0 -112
  198. package/src/lib/db/schema/sqlite-content.ts +0 -20
  199. package/src/lib/db/version-adapter.ts +0 -248
  200. package/src/lib/graphql/index.ts +0 -1
  201. package/src/lib/graphql/schema.ts +0 -443
  202. package/src/lib/rate-limit.ts +0 -267
  203. package/src/lib/storage.ts +0 -374
  204. package/src/lib/store.ts +0 -85
  205. package/src/middleware.ts +0 -177
  206. package/src/pages/admin/api-explorer.astro +0 -98
  207. package/src/pages/admin/graphql-explorer.astro +0 -40
  208. package/src/pages/admin/index.astro +0 -286
  209. package/src/pages/admin/keys.astro +0 -8
  210. package/src/pages/admin/rest-playground.astro +0 -44
  211. package/src/pages/admin/webhooks.astro +0 -8
  212. package/src/pages/api/[collection]/[id]/publish.ts +0 -52
  213. package/src/pages/api/[collection]/[id]/unpublish.ts +0 -42
  214. package/src/pages/api/[collection]/[id]/versions.ts +0 -66
  215. package/src/pages/api/[collection]/[id].ts +0 -213
  216. package/src/pages/api/[collection]/index.ts +0 -209
  217. package/src/pages/api/auth/[id].ts +0 -121
  218. package/src/pages/api/auth/audit-logs.ts +0 -57
  219. package/src/pages/api/auth/login.ts +0 -211
  220. package/src/pages/api/auth/logout.ts +0 -66
  221. package/src/pages/api/auth/me.ts +0 -36
  222. package/src/pages/api/auth/refresh.ts +0 -119
  223. package/src/pages/api/auth/register.ts +0 -188
  224. package/src/pages/api/auth/users.ts +0 -97
  225. package/src/pages/api/collections.ts +0 -59
  226. package/src/pages/api/globals/[slug].ts +0 -42
  227. package/src/pages/api/graphql.ts +0 -90
  228. package/src/pages/api/health.ts +0 -426
  229. package/src/pages/api/keys/[id].ts +0 -26
  230. package/src/pages/api/keys/index.ts +0 -75
  231. package/src/pages/api/media/[id].ts +0 -309
  232. package/src/pages/api/media/folders.ts +0 -609
  233. package/src/pages/api/media/index.ts +0 -146
  234. package/src/pages/api/media/resize.ts +0 -267
  235. package/src/pages/api/search.ts +0 -82
  236. package/src/pages/api/slug-availability.ts +0 -70
  237. package/src/pages/api/storage-config.ts +0 -20
  238. package/src/pages/api/storage-status.ts +0 -206
  239. package/src/pages/api/upload.ts +0 -334
  240. package/src/pages/api/webhooks/index.ts +0 -71
  241. package/src/pages/login.astro +0 -82
  242. package/src/pages/register.astro +0 -102
@@ -1,161 +1,7 @@
1
- import React, {
2
- createContext,
3
- useContext,
4
- useState,
5
- useEffect,
6
- type ReactNode,
7
- } from "react";
8
- import {
9
- defaultLightTheme,
10
- defaultDarkTheme,
11
- type ThemeConfig,
12
- } from "@kyro-cms/core/client";
13
-
14
- export type ThemeMode = "light" | "dark" | "system";
15
-
16
- interface ThemeContextValue {
17
- mode: ThemeMode;
18
- theme: ThemeConfig;
19
- setMode: (mode: ThemeMode) => void;
20
- setCustomTheme: (theme: ThemeConfig) => void;
21
- }
22
-
23
- const ThemeContext = createContext<ThemeContextValue | null>(null);
24
-
25
- export function useTheme() {
26
- const context = useContext(ThemeContext);
27
- if (!context) {
28
- // Return default light theme if used outside of a provider to prevent crashes
29
- return {
30
- mode: "light" as ThemeMode,
31
- theme: defaultLightTheme,
32
- setMode: () => {},
33
- setCustomTheme: () => {},
34
- };
35
- }
36
- return context;
37
- }
38
-
39
- interface ThemeProviderProps {
40
- children: ReactNode;
41
- defaultMode?: ThemeMode;
42
- themes?: {
43
- light?: ThemeConfig;
44
- dark?: ThemeConfig;
45
- };
46
- }
47
-
48
- export function ThemeProvider({
49
- children,
50
- defaultMode = "light",
51
- themes = {},
52
- }: ThemeProviderProps) {
53
- const [mode, setMode] = useState<ThemeMode>(defaultMode);
54
- const [customTheme, setCustomTheme] = useState<ThemeConfig | null>(null);
55
-
56
- const lightTheme = themes.light || defaultLightTheme;
57
- const darkTheme = themes.dark || defaultDarkTheme;
58
-
59
- const getResolvedTheme = (): ThemeConfig => {
60
- if (customTheme) return customTheme;
61
-
62
- if (mode === "system") {
63
- if (typeof window !== "undefined") {
64
- return window.matchMedia("(prefers-color-scheme: dark)").matches
65
- ? darkTheme
66
- : lightTheme;
67
- }
68
- return lightTheme;
69
- }
70
-
71
- return mode === "dark" ? darkTheme : lightTheme;
72
- };
73
-
74
- const [theme, setTheme] = useState<ThemeConfig>(lightTheme);
75
-
76
- useEffect(() => {
77
- const resolved = getResolvedTheme();
78
- setTheme(resolved);
79
- applyThemeVariables(resolved);
80
- }, [mode, customTheme]);
81
-
82
- useEffect(() => {
83
- if (mode !== "system") return;
84
-
85
- const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
86
- const handler = () => {
87
- const resolved = getResolvedTheme();
88
- setTheme(resolved);
89
- applyThemeVariables(resolved);
90
- };
91
-
92
- mediaQuery.addEventListener("change", handler);
93
- return () => mediaQuery.removeEventListener("change", handler);
94
- }, [mode, customTheme]);
95
-
96
- const applyThemeVariables = (config: ThemeConfig) => {
97
- const root = document.documentElement;
98
-
99
- if (config.colors) {
100
- Object.entries(config.colors).forEach(([key, value]) => {
101
- root.style.setProperty(`--kyro-${key}`, value);
102
- root.style.setProperty(
103
- `--kyro-${key}-light`,
104
- adjustBrightness(value, 0.9),
105
- );
106
- root.style.setProperty(
107
- `--kyro-${key}-dark`,
108
- adjustBrightness(value, 0.8),
109
- );
110
- });
111
- }
112
-
113
- if (config.borderRadius) {
114
- Object.entries(config.borderRadius).forEach(([key, value]) => {
115
- root.style.setProperty(`--kyro-radius-${key}`, value);
116
- });
117
- }
118
-
119
- if (config.fonts) {
120
- Object.entries(config.fonts).forEach(([key, value]) => {
121
- root.style.setProperty(`--kyro-font-${key}`, value);
122
- });
123
- }
124
- };
125
-
126
- const adjustBrightness = (hex: string, factor: number): string => {
127
- if (!hex.startsWith("#")) return hex;
128
-
129
- const r = parseInt(hex.slice(1, 3), 16);
130
- const g = parseInt(hex.slice(3, 5), 16);
131
- const b = parseInt(hex.slice(5, 7), 16);
132
-
133
- const adjust = (c: number) =>
134
- Math.round(c * factor)
135
- .toString(16)
136
- .padStart(2, "0");
137
-
138
- return `#${adjust(r)}${adjust(g)}${adjust(b)}`;
139
- };
140
-
141
- return (
142
- <ThemeContext.Provider
143
- value={{
144
- mode,
145
- theme,
146
- setMode,
147
- setCustomTheme,
148
- }}
149
- >
150
- {children}
151
- </ThemeContext.Provider>
152
- );
153
- }
154
-
155
- export const LightThemeProvider = (
156
- props: Omit<ThemeProviderProps, "defaultMode">,
157
- ) => <ThemeProvider defaultMode="light" {...props} />;
158
-
159
- export const DarkThemeProvider = (
160
- props: Omit<ThemeProviderProps, "defaultMode">,
161
- ) => <ThemeProvider defaultMode="dark" {...props} />;
1
+ export {
2
+ ThemeProvider,
3
+ LightThemeProvider,
4
+ DarkThemeProvider,
5
+ useTheme,
6
+ } from "../theme/ThemeProvider";
7
+ export type { ThemeMode } from "../theme/ThemeProvider";
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useEffect } from "react";
2
- import { apiGet, apiPatch } from "../lib/api";
2
+ import { apiGet, apiPatch, apiDelete } from "../lib/api";
3
3
  import {
4
4
  Users,
5
5
  UserPlus,
@@ -11,7 +11,15 @@ import {
11
11
  Clock,
12
12
  Search,
13
13
  Filter,
14
- } from "lucide-react";
14
+ Trash2,
15
+ Edit2,
16
+ ChevronRight,
17
+ ShieldCheck,
18
+ ShieldAlert
19
+ } from "./ui/icons";
20
+ import { useUIStore, toast } from "../lib/stores";
21
+ import { Badge } from "./ui/Badge";
22
+ import { PageHeader } from "./ui/PageHeader";
15
23
 
16
24
  interface User {
17
25
  id: string;
@@ -20,6 +28,7 @@ interface User {
20
28
  role: string;
21
29
  locked?: boolean;
22
30
  lastLogin?: string;
31
+ tenantId?: string;
23
32
  createdAt: string;
24
33
  }
25
34
 
@@ -27,6 +36,7 @@ export function UserManagement() {
27
36
  const [users, setUsers] = useState<User[]>([]);
28
37
  const [loading, setLoading] = useState(true);
29
38
  const [searchQuery, setSearchQuery] = useState("");
39
+ const { confirm, alert } = useUIStore();
30
40
 
31
41
  useEffect(() => {
32
42
  loadUsers();
@@ -35,7 +45,7 @@ export function UserManagement() {
35
45
  const loadUsers = async () => {
36
46
  try {
37
47
  setLoading(true);
38
- const result = await apiGet("/api/auth/users");
48
+ const result = await apiGet<any>("/api/users");
39
49
  setUsers(result.docs || []);
40
50
  } catch (error) {
41
51
  console.error("Failed to load users:", error);
@@ -44,15 +54,44 @@ export function UserManagement() {
44
54
  }
45
55
  };
46
56
 
47
- const handleToggleLock = async (user: User) => {
48
- try {
49
- await apiPatch(`/api/auth/${user.id}`, { locked: !user.locked });
50
- setUsers((prev) =>
51
- prev.map((u) => (u.id === user.id ? { ...u, locked: !u.locked } : u)),
52
- );
53
- } catch (error) {
54
- console.error("Failed to toggle user lock:", error);
55
- }
57
+ const handleToggleLock = (user: User) => {
58
+ const isLocking = !user.locked;
59
+ confirm({
60
+ title: isLocking ? "Lock User Account?" : "Unlock User Account?",
61
+ message: isLocking
62
+ ? `Are you sure you want to lock ${user.email}? They will be immediately logged out and unable to return.`
63
+ : `Restore system access for ${user.email}?`,
64
+ variant: isLocking ? "danger" : "success",
65
+ onConfirm: async () => {
66
+ try {
67
+ await apiPatch(`/api/users/${user.id}`, { locked: isLocking });
68
+ setUsers((prev) =>
69
+ prev.map((u) => (u.id === user.id ? { ...u, locked: isLocking } : u)),
70
+ );
71
+ toast.success(isLocking ? `Account locked: ${user.email}` : `Account restored: ${user.email}`);
72
+ } catch (error) {
73
+ console.error("Failed to toggle user lock:", error);
74
+ }
75
+ },
76
+ });
77
+ };
78
+
79
+ const handleDelete = (user: User) => {
80
+ confirm({
81
+ title: "Destroy User Account",
82
+ message: `You are about to permanently delete ${user.email}. This will remove all their data and cannot be undone.`,
83
+ variant: "danger",
84
+ confirmLabel: "Destroy Account",
85
+ onConfirm: async () => {
86
+ try {
87
+ await apiDelete(`/api/users/${user.id}`);
88
+ setUsers((prev) => prev.filter((u) => u.id !== user.id));
89
+ toast.success(`Identity purged: ${user.email}`);
90
+ } catch (error) {
91
+ console.error("Failed to delete user:", error);
92
+ }
93
+ },
94
+ });
56
95
  };
57
96
 
58
97
  const filteredUsers = users.filter(
@@ -62,152 +101,129 @@ export function UserManagement() {
62
101
  );
63
102
 
64
103
  return (
65
- <div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 px-8 pb-12">
104
+ <div className="w-full space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-700 px-8 pb-12">
66
105
  {/* Header */}
67
- <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6 pt-4">
68
- <div>
69
- <h1 className="text-4xl font-black tracking-tighter text-[var(--kyro-text-primary)]">
70
- Team <span className="text-[var(--kyro-primary)]">Management</span>
71
- </h1>
72
- <p className="text-[var(--kyro-text-secondary)] mt-1 font-medium opacity-60">
73
- Control access and oversee administrative governance.
74
- </p>
75
- </div>
76
- <div className="flex items-center gap-3">
77
- <button
78
- type="button"
79
- className="flex items-center gap-2 px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-2xl font-black text-sm shadow-xl active:scale-95 transition-all"
80
- >
81
- <UserPlus className="w-4 h-4" />
82
- Invite Member
83
- </button>
84
- </div>
85
- </div>
106
+ <PageHeader
107
+ title="Identity & Access"
108
+ description="Manage the core administrative team and security permissions."
109
+ icon={Users}
110
+ action={{
111
+ label: "New User",
112
+ onClick: () => {
113
+ // New user logic
114
+ },
115
+ icon: UserPlus,
116
+ }}
117
+ />
86
118
 
87
- {/* Tools bar */}
88
- <div className="flex flex-col md:flex-row gap-4">
119
+ {/* Control Bar */}
120
+ <div className="flex flex-col md:flex-row gap-3">
89
121
  <div className="relative flex-1 group">
90
- <Search className="absolute left-4 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" />
122
+ <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" />
91
123
  <input
92
124
  type="text"
93
- placeholder="Search by name or email..."
125
+ placeholder="Search by identity or email..."
94
126
  value={searchQuery}
95
127
  onChange={(e) => setSearchQuery(e.target.value)}
96
- className="w-full pl-11 pr-4 py-3 bg-[var(--kyro-bg-secondary)] border border-[var(--kyro-border)] rounded-2xl focus:outline-none focus:ring-2 focus:ring-[var(--kyro-primary)] focus:border-[var(--kyro-primary)] transition-all text-sm font-medium"
128
+ 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"
97
129
  />
98
130
  </div>
99
- <div className="flex items-center gap-3 bg-[var(--kyro-bg-secondary)] p-1 rounded-2xl border border-[var(--kyro-border)]">
100
- <button
101
- type="button"
102
- className="px-4 py-2 text-[10px] font-black uppercase tracking-widest bg-[var(--kyro-surface)] shadow-sm rounded-xl border border-[var(--kyro-border)]"
103
- >
104
- All Users
105
- </button>
106
- <button
107
- type="button"
108
- className="px-4 py-2 text-[10px] font-black uppercase tracking-widest opacity-40 hover:opacity-100 transition-all"
109
- >
110
- Admins
111
- </button>
112
- <button
113
- type="button"
114
- className="px-4 py-2 text-[10px] font-black uppercase tracking-widest opacity-40 hover:opacity-100 transition-all"
115
- >
116
- Restricted
117
- </button>
131
+ <div className="flex items-center gap-1 bg-[var(--kyro-surface-accent)] p-1 rounded-xl border border-[var(--kyro-border)]">
132
+ <button className="px-4 py-1.5 text-[10px] font-bold tracking-widest bg-[var(--kyro-surface)] shadow-sm rounded-lg border border-[var(--kyro-border)]">ALL</button>
133
+ <button className="px-4 py-1.5 text-[10px] font-bold tracking-widest opacity-40 hover:opacity-100 transition-all">ADMINS</button>
134
+ <button className="px-4 py-1.5 text-[10px] font-bold tracking-widest opacity-40 hover:opacity-100 transition-all">LOCKED</button>
118
135
  </div>
119
136
  </div>
120
137
 
121
- {/* User Grid */}
122
- <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
123
- {loading ? (
124
- Array(6)
125
- .fill(0)
126
- .map((_, i) => (
127
- <div
128
- key={i}
129
- className="surface-tile h-48 animate-pulse p-8 bg-[var(--kyro-bg-secondary)]"
130
- />
131
- ))
132
- ) : filteredUsers.length === 0 ? (
133
- <div className="col-span-full py-20 text-center surface-tile">
134
- <Users className="w-12 h-12 mx-auto opacity-10 mb-4" />
135
- <p className="opacity-40 italic">
136
- No team members found matching your search.
137
- </p>
138
- </div>
139
- ) : (
140
- filteredUsers.map((user) => (
141
- <div
142
- key={user.id}
143
- className={`surface-tile p-8 group transition-all duration-500 hover:shadow-2xl relative overflow-hidden ${user.locked ? "grayscale opacity-60" : ""}`}
144
- >
145
- {/* Status Badge */}
146
- <div className="absolute top-0 right-0 p-6">
147
- <span
148
- className={`inline-flex items-center px-2 py-0.5 rounded-full text-[8px] font-black uppercase tracking-widest ${user.locked ? "bg-red-500/10 text-red-500" : "bg-green-500/10 text-green-500"}`}
149
- >
150
- {user.locked ? "Locked" : "Active"}
151
- </span>
152
- </div>
153
-
154
- <div className="flex items-center gap-4 mb-6">
155
- <div className="w-14 h-14 rounded-2xl bg-[var(--kyro-bg-secondary)] border border-[var(--kyro-border)] flex items-center justify-center text-xl font-black text-[var(--kyro-primary)] group-hover:scale-110 transition-transform duration-500">
156
- {user.name ? user.name[0] : user.email[0].toUpperCase()}
157
- </div>
158
- <div>
159
- <h3 className="text-lg font-black tracking-tight">
160
- {user.name || "Set Name"}
161
- </h3>
162
- <div className="flex items-center gap-2 text-[11px] font-bold opacity-40 uppercase tracking-wider">
163
- <Shield className="w-3 h-3" />
164
- {user.role}
165
- </div>
166
- </div>
167
- </div>
168
-
169
- <div className="space-y-4 mb-8">
170
- <div className="flex items-center gap-3 text-sm font-medium text-[var(--kyro-text-secondary)]">
171
- <Mail className="w-4 h-4 opacity-40" />
172
- <span className="truncate">{user.email}</span>
173
- </div>
174
- <div className="flex items-center gap-3 text-sm font-medium text-[var(--kyro-text-secondary)]">
175
- <Clock className="w-4 h-4 opacity-40" />
176
- <span>
177
- Last seen{" "}
178
- {user.lastLogin
179
- ? new Date(user.lastLogin).toLocaleDateString()
180
- : "Never"}
181
- </span>
182
- </div>
183
- </div>
184
-
185
- <div className="flex items-center gap-3">
186
- <button
187
- type="button"
188
- onClick={() => handleToggleLock(user)}
189
- className={`flex-1 py-2.5 rounded-xl font-black text-[10px] uppercase tracking-widest transition-all flex items-center justify-center gap-2 ${user.locked ? "bg-green-500/10 text-green-500 hover:bg-green-500/20" : "bg-red-500/10 text-red-500 hover:bg-red-500/20"}`}
190
- >
191
- {user.locked ? (
192
- <Unlock className="w-3 h-3" />
193
- ) : (
194
- <Lock className="w-3 h-3" />
195
- )}
196
- {user.locked ? "Unlock Account" : "Lock Account"}
197
- </button>
198
- <button
199
- type="button"
200
- className="p-2.5 bg-[var(--kyro-bg-secondary)] rounded-xl border border-[var(--kyro-border)] hover:bg-[var(--kyro-surface)] transition-all"
201
- >
202
- <MoreVertical className="w-4 h-4 opacity-40" />
203
- </button>
204
- </div>
205
-
206
- {/* Decorative detail */}
207
- <div className="absolute -bottom-4 -right-4 w-24 h-24 bg-[var(--kyro-primary)] opacity-0 group-hover:opacity-5 blur-3xl transition-opacity" />
208
- </div>
209
- ))
210
- )}
138
+ {/* User Table */}
139
+ <div className="surface-tile overflow-hidden">
140
+ <table className="w-full text-left table-fixed">
141
+ <thead>
142
+ <tr className="text-[var(--kyro-text-secondary)] font-bold text-[9px] tracking-[0.2em] uppercase border-b border-[var(--kyro-border)]">
143
+ <th className="px-6 py-4 w-64">Member Identity</th>
144
+ <th className="px-6 py-4">Administrative Role</th>
145
+ <th className="px-6 py-4">Security Status</th>
146
+ <th className="px-6 py-4">Last Activity</th>
147
+ <th className="px-6 py-4 w-32 text-right">Actions</th>
148
+ </tr>
149
+ </thead>
150
+ <tbody className="divide-y divide-[var(--kyro-border)]">
151
+ {loading ? (
152
+ Array.from({ length: 6 }).map((_, i) => (
153
+ <tr key={i} className="animate-pulse">
154
+ <td colSpan={5} className="px-6 py-5 bg-[var(--kyro-surface-accent)]/30" />
155
+ </tr>
156
+ ))
157
+ ) : filteredUsers.length === 0 ? (
158
+ <tr>
159
+ <td colSpan={5} className="px-6 py-20 text-center">
160
+ <p className="text-xs font-bold opacity-30 tracking-widest uppercase italic">No identity matches found</p>
161
+ </td>
162
+ </tr>
163
+ ) : (
164
+ filteredUsers.map((user) => (
165
+ <tr key={user.id} className={`hover:bg-[var(--kyro-surface-accent)]/50 transition-colors group ${user.locked ? "opacity-50 grayscale" : ""}`}>
166
+ <td className="px-6 py-3.5">
167
+ <div className="flex items-center gap-3">
168
+ <div className="w-8 h-8 rounded-lg bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] flex items-center justify-center text-xs font-bold text-[var(--kyro-primary)] group-hover:scale-105 transition-transform">
169
+ {user.name ? user.name[0] : user.email[0].toUpperCase()}
170
+ </div>
171
+ <div className="min-w-0">
172
+ <div className="flex items-center gap-2">
173
+ <div className="text-xs font-bold text-[var(--kyro-text-primary)] truncate">{user.name || user.email.split("@")[0]}</div>
174
+ {user.tenantId && (
175
+ <Badge variant="outline" className="text-[7px] px-1 py-0 border-none bg-[var(--kyro-surface-accent)] opacity-50">
176
+ {user.tenantId}
177
+ </Badge>
178
+ )}
179
+ </div>
180
+ <div className="text-[10px] text-[var(--kyro-text-secondary)] opacity-50 truncate">{user.email}</div>
181
+ </div>
182
+ </div>
183
+ </td>
184
+ <td className="px-6 py-3.5">
185
+ <div className="flex items-center gap-2">
186
+ <Shield className="w-3.5 h-3.5 opacity-30" />
187
+ <span className="text-[10px] font-bold tracking-widest uppercase opacity-70">{user.role}</span>
188
+ </div>
189
+ </td>
190
+ <td className="px-6 py-3.5">
191
+ <Badge variant={user.locked ? "danger" : "success"} dot className="text-[8px] font-bold uppercase tracking-widest">
192
+ {user.locked ? "Restricted" : "Authorized"}
193
+ </Badge>
194
+ </td>
195
+ <td className="px-6 py-3.5">
196
+ <div className="flex items-center gap-2 text-[10px] font-bold text-[var(--kyro-text-secondary)] opacity-50 uppercase tabular-nums">
197
+ <Clock className="w-3 h-3" />
198
+ {user.lastLogin ? new Date(user.lastLogin).toLocaleDateString() : "Never"}
199
+ </div>
200
+ </td>
201
+ <td className="px-6 py-3.5 text-right">
202
+ <div className="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-all">
203
+ <button
204
+ onClick={() => handleToggleLock(user)}
205
+ className={`p-1.5 rounded-lg border transition-all ${user.locked ? "bg-green-500/10 text-green-500 border-green-500/20 hover:bg-green-500/20" : "bg-amber-500/10 text-amber-500 border-amber-500/20 hover:bg-amber-500/20"}`}
206
+ title={user.locked ? "Restore Access" : "Restrict Access"}
207
+ >
208
+ {user.locked ? <Unlock className="w-3.5 h-3.5" /> : <Lock className="w-3.5 h-3.5" />}
209
+ </button>
210
+ <button
211
+ onClick={() => handleDelete(user)}
212
+ 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"
213
+ title="Delete User"
214
+ >
215
+ <Trash2 className="w-3.5 h-3.5" />
216
+ </button>
217
+ <button className="p-1.5 rounded-lg border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface)] transition-all">
218
+ <MoreVertical className="w-3.5 h-3.5" />
219
+ </button>
220
+ </div>
221
+ </td>
222
+ </tr>
223
+ ))
224
+ )}
225
+ </tbody>
226
+ </table>
211
227
  </div>
212
228
  </div>
213
229
  );
@@ -0,0 +1,110 @@
1
+ import React from "react";
2
+ import { Dropdown, DropdownItem, DropdownSeparator } from "./ui/Dropdown";
3
+ import { User, Shield, Key, Webhook, Clock, FileText, ExternalLink, HelpCircle, LogOut } from "./ui/icons";
4
+
5
+ interface UserMenuProps {
6
+ adminPath: string;
7
+ }
8
+
9
+ export function UserMenu({ adminPath }: UserMenuProps) {
10
+
11
+ return (
12
+ <Dropdown
13
+ align="right"
14
+ trigger={
15
+ <div
16
+ className="flex justify-center p-2.5 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface)] rounded-xl transition-all shadow-sm active:scale-95"
17
+ title="Account"
18
+ >
19
+ <User className="w-4 h-4" strokeWidth={2.5} />
20
+ </div>
21
+ }
22
+ >
23
+ <div className="px-4 py-2 mb-1">
24
+ <p className="text-[10px] font-medium tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40">
25
+ Account
26
+ </p>
27
+ </div>
28
+
29
+ <DropdownItem
30
+ icon={<User className="w-4 h-4" />}
31
+ onClick={() => window.location.href = `${adminPath}/settings/account`}
32
+ >
33
+ Profile Settings
34
+ </DropdownItem>
35
+
36
+ <DropdownItem
37
+ icon={<Shield className="w-4 h-4" />}
38
+ onClick={() => window.location.href = `${adminPath}/roles`}
39
+ >
40
+ Permissions
41
+ </DropdownItem>
42
+
43
+ <DropdownSeparator />
44
+
45
+ <div className="px-4 py-2 mb-1">
46
+ <p className="text-[10px] font-medium tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40">
47
+ Developer
48
+ </p>
49
+ </div>
50
+
51
+ <DropdownItem
52
+ icon={<Key className="w-4 h-4" />}
53
+ onClick={() => (window.location.href = `${adminPath}/keys`)}
54
+ >
55
+ API Keys
56
+ </DropdownItem>
57
+
58
+ <DropdownItem
59
+ icon={<Webhook className="w-4 h-4" />}
60
+ onClick={() => (window.location.href = `${adminPath}/webhooks`)}
61
+ >
62
+ Web Hooks
63
+ </DropdownItem>
64
+ <DropdownItem
65
+ icon={<Clock className="w-4 h-4" />}
66
+ onClick={() => (window.location.href = `${adminPath}/sessions`)}
67
+ >
68
+ Sessions
69
+ </DropdownItem>
70
+ <DropdownItem
71
+ icon={<FileText className="w-4 h-4" />}
72
+ onClick={() => (window.location.href = `${adminPath}/audit`)}
73
+ >
74
+ Audit Logs
75
+ </DropdownItem>
76
+
77
+ <DropdownSeparator />
78
+
79
+ <div className="px-4 py-2 mb-1">
80
+ <p className="text-[10px] font-medium tracking-[0.2em] text-[var(--kyro-text-secondary)] opacity-40">
81
+ Resources
82
+ </p>
83
+ </div>
84
+
85
+ <DropdownItem
86
+ icon={<ExternalLink className="w-4 h-4" />}
87
+ onClick={() => window.open("https://docs.kyro.dev", "_blank")}
88
+ >
89
+ Documentation
90
+ </DropdownItem>
91
+
92
+ <DropdownItem
93
+ icon={<HelpCircle className="w-4 h-4" />}
94
+ onClick={() => window.open("https://github.com/danielDozie/kyro-cms/issues", "_blank")}
95
+ >
96
+ Get Support
97
+ </DropdownItem>
98
+
99
+ <DropdownSeparator />
100
+
101
+ <DropdownItem
102
+ icon={<LogOut className="w-4 h-4" />}
103
+ danger
104
+ onClick={() => document.getElementById("logout-btn")?.click()}
105
+ >
106
+ Sign Out
107
+ </DropdownItem>
108
+ </Dropdown>
109
+ );
110
+ }